1 điểm bởi GN⁺ 2 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • scriptc biên dịch TypeScript thông thường thành binary native nhỏ gọn chạy không cần Node, V8 hay JavaScript engine, đồng thời duy trì kiểm tra kiểu của trình biên dịch TypeScript thực tế và khả năng tương thích hành vi với Node
  • Tùy theo cấu trúc mã, công cụ xác định liệu có thể biên dịch tĩnh hay không và mặc định tạo mã native; chỉ khi chọn --dynamic mới dùng quickjs-ng để chạy JavaScript từ các gói npm và mã có kiểu any
  • Hỗ trợ từ class, generic, async/await, exception, regex cho đến API server của Node, fetch và dependency npm; các cú pháp chưa hỗ trợ sẽ bị từ chối kèm mã lỗi, code frame và gợi ý sửa
  • Chạy hơn 800 chương trình trên Node và binary native để so sánh output và mã thoát, đồng thời kiểm tra lỗi bộ nhớ bằng AddressSanitizer và audit số lần tham chiếu
  • Trong phép đo trên Apple M series, thời gian khởi động khoảng 2,4ms, binary tĩnh 170–200KB, RSS thông thường 1–4MB; khi gồm chế độ động và dependency nhúng, binary khoảng 3MB

Mô hình biên dịch tĩnh

  • Sử dụng TypeScript hiện có, không cần dialect hay annotation riêng, áp dụng mức độ nghiêm ngặt kiểm tra của tsconfig.json và thư viện es2025 thực tế của TypeScript
    • Nếu dự án có @types/node thì cũng kiểm tra kiểu cùng với nó
    • Mã có thể được đi tới nhưng không có lowering sẽ đưa ra chẩn đoán chính xác và dừng biên dịch
  • scriptc coverage hiển thị số câu lệnh đã phân tích, tỷ lệ được biên dịch tĩnh, các yếu tố chặn và mã lỗi
    • Trong ví dụ, 99%, tức 4.451 trên 4.481 câu lệnh, được biên dịch tĩnh
  • Cách thực thi được phân tách rõ thành ba giai đoạn
    1. Biên dịch tĩnh: chế độ mặc định, chuyển đổi thành mã native không cần JavaScript engine
    2. Thực thi động: nếu chỉ định --dynamic, sẽ bao gồm khoảng 620KB quickjs-ng để chạy JavaScript từ các gói npm và mã kiểu any
      • Mọi giá trị đi vào mã tĩnh đều được kiểm chứng ở runtime
      • Nếu giá trị khác kiểu đã khai báo, ném TypeError có thể bắt được mà không làm hỏng bộ nhớ
    3. Từ chối: mã không thể xử lý sẽ được báo kèm mã lỗi, code frame và thường có gợi ý sửa; không âm thầm biên dịch sai

TypeScript và thư viện chuẩn được hỗ trợ

  • Tính năng ngôn ngữ hỗ trợ class kế thừa đơn và dynamic dispatch, closure, đơn hình hóa generic, discriminated union, destructuring, spread, template literal, getter/setter và iterator
    • Khi chứng minh được an toàn, dynamic dispatch sẽ được devirtualize
    • Discriminated union được xử lý bằng giá trị tag dựa trên narrowing của TypeScript
    • async/await được triển khai bằng stack-pool fiber và scheduling phù hợp với JavaScript
    • Hỗ trợ exception và finally, tham số tùy chọn, mặc định và rest parameter
  • Regex dùng trình thông dịch bytecode tương thích ECMAScript giống QuickJS, và chỉ liên kết vào binary có sử dụng regex
  • Thư viện chuẩn gồm string tuân theo ngữ nghĩa UTF-16, Array/Map/Set có cùng quy tắc thứ tự và đồng nhất như JavaScript
    • Chuyển đổi kiểu của JSON đi qua kiểm chứng runtime
    • Cũng cung cấp Math, typed array, Buffer và hệ phân cấp Error hỗ trợ catch có kiểu

Node và Web API

  • Node API hỗ trợ fs, path, process, child_process, os, crypto, url/URL, zlib, timer và signal handler
    • fs cung cấp API đồng bộ và Promise
    • child_process hỗ trợ stream pipe
    • Event loop không có dependency bên ngoài
  • Server stack bao gồm net, http, https, tls, dgram, dns, fs.watch, readline và có thể biên dịch proxy server thực tế
    • TLS dùng mbedTLS được tích hợp
  • Một phần WHATWG Web API như fetch, stream, Headers, AbortSignal được triển khai trên cùng native network/TLS stack
    • Hỗ trợ redirect, gzip, AbortSignal.timeout, nguyên nhân lỗi theo kiểu Node
    • Không dùng libcurl hay dependency HTTP của hệ thống

Dependency npm và thực thi động

  • Trong --dynamic, sử dụng cách phân giải module của Node và kiểm tra kiểu dựa trên .d.ts do package cung cấp
  • JavaScript của gói npm được đưa vào binary khi build nên trong lúc chạy không đọc node_modules
  • scriptc coverage --dynamic hiển thị từng câu lệnh được chạy ở vùng tĩnh hay động và các yếu tố chặn còn lại
  • JavaScript engine chỉ được bao gồm khi người dùng chọn rõ chế độ động, nên kích thước binary không âm thầm tăng lên

Độ chính xác và an toàn bộ nhớ

  • Kiểm thử vi sai chạy hơn 800 chương trình lần lượt trên Node và binary native để so sánh stdout, stderr và mã thoát theo từng byte
    • Output số tuân theo biểu diễn round-trip ngắn nhất, và được fuzzing kiểm chứng bằng cách so sánh 1 triệu giá trị double với Node
    • Server được kiểm thử bằng cách kết nối driver client thực tế vào cả hai implementation
  • Toàn bộ bộ kiểm thử được chạy lại dưới AddressSanitizer và audit số lần tham chiếu; nếu có rò rỉ hoặc use-after-free thì build thất bại
  • Các hành vi cố ý khác Node có vài chục trường hợp, chủ yếu liên quan đến nội bộ timing và thuộc tính object lỗi
    • Mỗi khác biệt đều được tài liệu hóa và đánh số; không cho phép khác biệt ẩn

Đặc tính hiệu năng

  • Đo trên Apple M series, dựa trên cùng tác vụ và output giống nhau từng byte với Node, Go, Rust, Zig
  • Thời gian khởi động khoảng 2,4ms, ngắn hơn khoảng 47ms của Node, tương tự Zig và vượt Go/Rust
  • Kích thước binary tĩnh là 170–200KB; nếu gồm --dynamic và dependency nhúng thì khoảng 3MB
    • Binary Go được đưa ra để so sánh khoảng 2MB, Node SEA là 60–100MB
  • Mức dùng bộ nhớ thông thường là RSS 1–4MB, trong khi Node là 67–116MB
  • Runtime cạnh tranh với ngôn ngữ hệ thống trong hầu hết tác vụ, đồng thời duy trì ngữ nghĩa f64 phù hợp với JavaScript
    • Suy luận số nguyên và phân tích quyền sở hữu nằm trong roadmap

Lối thoát rõ ràng

  • comptime(() => ...) chạy TypeScript ở thời điểm build trong VM biệt lập bên trong compiler và đưa kết quả vào binary dưới dạng literal
  • --ffi liên kết trực tiếp khai báo TypeScript chỉ có signature với lời gọi C ABI, đồng thời liên kết archive, object và thư viện hệ thống được khai báo trong manifest
    • Ranh giới là rõ ràng và có kèm thông tin độ dài
    • Cách làm chi tiết có trong Native FFI guide
  • Type assertion có kiểm tra như JSON.parse(...) as Config sẽ chèn mã kiểm chứng runtime
    • Khi kiểm chứng thất bại, ném exception chứa đường dẫn sai và kiểu kỳ vọng/thực tế, chẳng hạn expected number at $.port, got string

Cấu trúc compiler

  • Quy trình xử lý theo thứ tự TypeScript → tsc parsing/kiểm tra kiểu → lowering → typed IR → C → clang → file thực thi native
  • packages/compiler gồm frontend dựa trên tsc API, kiểm chứng/serialize IR, backend LLVM và C
    • Chỉ IR được dùng làm interface giữa frontend và backend
    • LLVM là bộ sinh mã mặc định; với chương trình ngoài phạm vi hỗ trợ sẽ dùng đường thay thế minh bạch
    • C được duy trì như backend tham chiếu lâu dài, và --backend c tạo kết quả dễ đọc có thông tin dòng nguồn
  • packages/runtime triển khai giá trị dựa trên số lần tham chiếu và bộ thu gom vòng tham chiếu, stack-pool fiber, event loop kqueue, server stack và output số tương thích JavaScript
    • Dùng liên kết theo tính năng để chỉ đưa các tính năng thực sự được dùng vào binary
  • packages/cli cung cấp các lệnh scriptc build, scriptc run, scriptc coverage

Cài đặt và phát triển

  • Cài bằng npm install -g scriptc và cần clang
  • Nền tảng chính là macOS arm64; binary Linux và Windows được cross-compile
    • Mỗi nền tảng được kiểm chứng bằng một tuyến kiểm thử vi sai riêng
  • Bắt đầu phát triển với pnpm install && pnpm build
    • pnpm test chạy bộ kiểm thử vi sai và snapshot chẩn đoán
    • SCRIPTC_SAN=1 pnpm test chạy cùng các kiểm thử dưới ASan và audit số lần tham chiếu
    • pnpm scriptc build x.ts --emit-ir giữ lại C đã sinh và x.ir.json
  • Mọi tính năng đều được thêm cùng kiểm thử vi sai, và chỉ có thể merge khi cả kiểm thử thông thường lẫn kiểm thử an toàn bộ nhớ đều qua

1 bình luận

 
Ý kiến trên Hacker News
  • Vercel dường như cứ khoảng mỗi tháng lại tung ra một dự án gây chú ý để duy trì độ tin cậy và sự hiện diện. Không nghĩ một công ty hay dự án nghiêm túc nào sẽ dùng scriptc.
    Tôn trọng những người đóng góp, nhưng mã trông rất giống được tạo bằng Claude, và việc Claude không được ghi là người đóng góp càng khiến nó đáng ngờ hơn.

    • Đúng là một đóng góp có quy mô khổng lồ ;-) commit
    • Cùng người dẫn dắt vibe coding đó cũng dẫn dắt zerolang, “ngôn ngữ lập trình dành cho agent” mà Vercel công bố rầm rộ hồi tháng 5. Sau khi đẩy lên 1.200 commit, phát triển dừng lại vào khoảng giữa tháng 6.
      Dự án / bài HN liên quan
    • Simon Willison có vẻ chỉ sửa README, và dường như không thực sự tham gia vào dự án.
    • Nhìn vào lịch sử đóng góp, có vẻ một người đã vibe coding khoảng 99% dự án, và cũng không thấy có nền tảng liên quan đến compiler.
    • Nhiều sản phẩm SaaS đang hợp tác với Vercel, và trong công cụ phát triển, Next.js và React được xem như các SDK hàng đầu.
  • Porffor đã theo đuổi cùng mục tiêu này một thời gian. Nhà phát triển CanadaHonk cực kỳ xuất sắc, nhưng dự án vẫn chỉ vượt qua khoảng 68% Test262.
    Nếu không phải tôi đang hiểu sai phạm vi dự án, thì cách Vercel đạt tiến triển nhanh như vậy khá đáng nghi.

  • Đây là kiểu dự án Vercel điển hình. Mới công bố 5 ngày, hoàn toàn vibe coding, nhận 1.500 sao không rõ vì sao nhưng không giải quyết vấn đề của ai cả, và có lẽ tối đa vài tháng nữa sẽ ngừng bảo trì.

  • Thay vì chỉ phê bình, tôi đã thử áp dụng nó trực tiếp cho nhiều dự án local, nhưng tất cả đều phát sinh hàng trăm lỗi ở bước phân tích phạm vi mã, nên về cơ bản là không dùng được.
    Nếu viết từ đầu mà không dùng thư viện ngoài thì có thể compile thành binary, nhưng khi đó không có lý do gì để không dùng những ngôn ngữ vốn được thiết kế ngay từ đầu để biên dịch đúng nghĩa như Rust, Go, Zig, D, C, V, Ada, C++, Nim, Swift, Kotlin Native, Haskell.

  • Điểm mạnh của TypeScript không chỉ là khả năng biểu đạt, mà còn là khả năng tương thích với hệ sinh thái npm khổng lồ. Phần lớn package chỉ định nghĩa interface bằng khai báo type và phân phối mã thực tế dưới dạng JavaScript, nên để dùng package thì trên thực tế cần JavaScript engine.
    Nếu định bắt đầu hoàn toàn từ đầu và không dùng bất kỳ package npm nào, dùng AssemblyScript sẽ tốt hơn. Node khuyến nghị rõ ràng không phân phối package bằng TypeScript, vì TypeScript không tương thích ngược ngay cả giữa các phiên bản minor, và cấu hình compiler cũng không có tính di động giữa các package.

    • scriptc có vẻ xử lý các dependency kiểu này bằng cách tùy chọn đưa engine quickjs-ng 620KB vào bundle khi cần chạy chúng.
    • Tôi muốn dùng nó cho các công cụ dòng lệnh có mục đích rõ ràng cần chia sẻ mã với một dự án TypeScript lớn hơn, hơn là cho mã có nhiều dependency.
    • Đây là lý do để phân phối thư viện không có type, nhưng khó hiểu khi kết luận rằng lý do đó vượt trội hơn giá trị của type.
  • Chỉ cần một dự án như thế này là có thể liên tục xuất hiện trên trang nhất của các dịch vụ như HN. Đây là chiến lược tăng trưởng: đầu tư token để tạo một dự án trông có vẻ hấp dẫn nhưng không ai muốn, công bố công khai để mở rộng độ phủ, rồi lặp lại.
    Sau 12 tháng, có thể 90% dự án mã nguồn mở sẽ là kết quả vibe coding chỉ trông thú vị nhưng không có người dùng thực sự. Giờ có thể dễ dàng tạo cả compiler hoàn chỉnh, nhưng cốt lõi vẫn là bảo trì dài hạn và cộng đồng; một tiêu đề bắt mắt thôi không giữ được người dùng.
    Nếu Vercel nghiêm túc, họ nên chấp nhận chi phí và rủi ro thực tế bằng cách dùng nó làm runtime thử nghiệm của chính mình.

  • Đây là một miền vấn đề rất hay. Tôi đã áp dụng một việc tương tự cho Zod: tạo compiler tối ưu mã runtime bằng AI: zod-compiler.
    Nó compile schema Zod tại thời điểm build thành chuỗi phép toán boolean đơn giản, giúp nhanh hơn 2–74 lần mà không cần đổi mã, và plugin thay thế các lời gọi Zod bằng parsing đã compile. Phần lớn tối ưu hóa do Claude viết qua hơn 100 vòng lặp.
    Giống scriptc ở chỗ có thể so sánh kết quả với Zod thật, nên không cần đánh giá độ chính xác một cách chủ quan. Cách này có thể áp dụng cho compiler, công cụ serialization, formatter, query planner, v.v. nếu có triển khai chuẩn và benchmark.

  • Tôi đã dùng Claude để chạy benchmark scriptc và Node. Ngay cả trong kết quả mảng byte có lợi nhất, sau tối ưu hóa chuyên biệt, scriptc vẫn chậm hơn Node 24 khoảng 7,5 lần.
    Đổi lại, executable khởi động nhanh hơn 12 lần (1,5ms so với 18,6ms), dùng bộ nhớ ít hơn 72 lần (2,5MiB so với 181MiB), và tạo thành một executable đơn 370KB không có dependency runtime.

  • Việc thừa nhận nhu cầu về executable native nhỏ và nhanh là tốt, nhưng nhìn vào những gì Java đã trải qua trong nhiều thập kỷ thì tôi hoài nghi về tính thực dụng. GCJ của thập niên 1990 về mặt kỹ thuật khá ổn, nhưng không có hỗ trợ hệ sinh thái.
    Sau đó GraalVM Native xử lý vấn đề toàn diện hơn, khiến các thư viện và framework lớn bắt đầu đảm bảo tương thích, nhưng ngay cả hiện nay việc chạy native hoàn hảo một ứng dụng hiện có đơn giản vẫn rất khó. Những nỗ lực như scriptc là đáng hoan nghênh, nhưng con đường đến thực dụng hóa có lẽ sẽ dài và gập ghềnh.

    • Theo tôi biết, nhóm Graal cũng đã thử một cách tiếp cận meta-interpreter tương tự. Những thứ mà thực thi native không xử lý được như nạp bytecode động hay reflection thì họ định diễn giải bằng triển khai Java Espresso.
    • GCJ luôn gần với prototype hơn. Người dùng nghiêm túc hẳn đã mua các JDK thương mại có công cụ AOT như Excelsior JET, BEA JRockit.
      Một trong những lý do Excelsior biến mất có thể là vì GraalVM và OpenJ9 được cung cấp miễn phí. PTC và Aicas vẫn hoạt động tốt nhờ nhóm khách hàng embedded/thời gian thực ít được chú ý.
  • Nếu tiếp tục được phát triển, nó có tiềm năng trở thành một thành tựu lớn ngang tầm .NET AOT. Vì mới công bố vài ngày nên hiện giờ chỉ nên thử nhẹ, nhưng nếu không bị bỏ rơi và tiếp tục tiến triển, nó có thể giúp ích đáng kể cho hệ sinh thái.
    Mã do AI tạo cũng có biên độ chất lượng rộng như mã do con người viết. Với phần mềm quan trọng, cần tạo mã theo cùng tiêu chuẩn như khi tự viết và review toàn bộ mã; nếu dùng như vậy thì đây là một cách tuyệt vời. Các dự án thiếu review có khả năng chất lượng và tinh thần chịu trách nhiệm của nhà phát triển thấp hơn, nên khó được chấp nhận hơn.
    Thảo luận online thường trôi về hai cực “toàn bộ do AI tạo” và “tuyệt đối không dùng AI”, nhưng trong thực tế, điểm giữa giúp tăng tốc phán đoán thận trọng mới là hợp lý. Những phần mềm không như vậy sẽ khiến người ta ngần ngại sử dụng vì rủi ro chất lượng thấp hoặc bị bỏ mặc.