
Gleam 編譯器已知痛點清單深度解析JavaScript 打包、編譯期工作分配與自定義類型表示優化【免費下載鏈接】gleam?? A friendly language for building type-safe, scalable systems!項目地址: https://gitcode.com/GitHub_Trending/gl/gleam本篇技術指南聚焦 Gleam 編譯倉庫內的 docs/annoyances.md 這份已知痛點清單逐條剖析其中記錄的三個懸而未決的技術問題編譯后 JavaScript 的打包不夠直觀、重復工作無法遷移到編譯期或初始化期、以及 JavaScript 后端自定義類型的運行時表示仍有性能優化空間。通過對照 compiler-core/src/javascript.rs、compiler-core/templates/prelude.mjs 與 compiler-core/src/config.rs 等源碼實現讀者可以理解這些問題產生的底層原因、當前編譯器的真實處理方式以及未來可能的設計方向。文檔定位一份編譯器維護者的優化路線圖在 Gleam 倉庫中docs/annoyances.md是一份特殊的技術文檔它既不是用戶手冊也不是變更日志而是編譯器維護者親手記錄的、在編寫 Gleam 代碼過程中遇到并希望未來解決的設計性痛點清單。文檔開篇明確說明This document contains a list of issues and annoyances that we have writing Gleam code today, so that we can devise solutions to them in future.本文檔記錄了我們在編寫 Gleam 代碼時遇到的各類問題與不便之處以便未來為它們設計解決方案。同時文檔也劃定了邊界已經存在已知解決方案、但尚未實現的痛點不會記錄在這份文檔里而是跟蹤在 Gleam 的 issue 跟蹤器中。這意味著docs/annoyances.md只收錄那些**問題清晰、方案尚不明確**的設計課題——它實質上是一份面向編譯器設計者的開放問題清單每個條目都代表一個值得深入研究的優化方向。目前文檔中記錄著三個主題Bundling compiled JavaScript is non-obvious打包編譯后的 JavaScript 不夠直觀Cannot shift repeated work to compile or initialise time無法將重復性工作轉移到編譯期或初始化期JavaScript custom type representation could be fasterJavaScript 自定義類型表示可以更快下文將逐條展開并結合當前倉庫的源碼實現說明每個問題的具體背景。痛點一打包編譯后的 JavaScript 不夠直觀現狀每個 Gleam 模塊編譯為一個獨立 ESM 模塊Gleam 的 JavaScript 后端位于 compiler-core/src/javascript.rs采用逐模塊編譯的策略每個.gleam源文件被編譯成獨立的.mjs文件模塊之間通過 ES Moduleimport/export互相引用。從Generator::compile的組裝順序javascript.rs可以清晰看到每個輸出文件的完整結構sourcemap 引用sourcemap_reference當開啟源碼映射時輸出文件末尾會附帶//# sourceMappingURLmodule.mjs.map注釋TypeScript 聲明引用type_reference當開啟 TypeScript 聲明生成時會附帶對./module.d.mts的引用導入語句imports.into_doc由collect_imports收集模塊自身的 import、外部函數 FFI 導入以及按需注冊的 prelude 函數模塊定義definitions依次輸出自定義類型、常量、函數echo 定義echo_definition當代碼中使用echo調試表達式時注入 echo.mjs 模板。其中最關鍵的是按需導入 prelude機制Generator內部維護一個UsageTracker在生成表達式代碼時記錄哪些 prelude 功能被用到如Ok/Error、toList、prepend、isEqual、CustomType、位數組系列函數等最后在compile收尾階段通過register_prelude_usage為實際用到的功能生成精確的導入語句javascript.rs。這保證了每個.mjs文件只導入自己真正依賴的運行時函數。源碼級證據prelude 是唯一的公共運行時所有的 JavaScript 運行時支撐代碼集中在一個文件中compiler-core/templates/prelude.mjs它通過include_str!直接嵌入編譯器二進制見 javascript.rs 的pub const PRELUDE: str include_str!(../templates/prelude.mjs)。這個文件定義了CustomType基類含withFields方法用于記錄更新List類及Empty/NonEmpty子類并實現了Symbol.iterator迭代器、toArray()、atLeastLength()、countLength()等高效輔助方法BitArray類支持非字節對齊的位偏移bitOffset、位切片、整數/浮點讀寫等UtfCodepoint類以及toList、prepend、isEqual等核心函數。由于每個模塊最終都要引用 prelude或其派生出的模塊打包器必須能夠正確解析這類跨模塊的 ESM 依賴關系。為什么打包成為痛點對于一個要發布到瀏覽器或 Node.js 生產環境的 Gleam 項目開發者需要把幾十上百個.mjs文件合并成少量 bundle。痛點在于Gleam 編譯器本身不負責打包——從 compiler-cli 的源碼結構看CLI 只負責編譯、運行與發布產物就是分散的 ESM 文件輸出文件包含多種附加引用——sourcemap、.d.mts類型聲明引用、按需生成的 prelude 導入打包配置需要理解并正確處理這些引用否則很容易出現打包后類型聲明丟失或sourcemap 失效的問題運行時是預編譯共享庫——prelude.mjs 中Empty、List$Empty$const這類單例常量的語義依賴模塊共享打包器的 tree-shaking 或作用域隔離如果處理不當可能破壞單例標識語義。正因如此文檔把打包編譯后的 JavaScript列為需要改進的體驗問題。目前倉庫中的做法是項目本身依賴外部打包工具如 esbuild、rollup、webpack 等通用 JS 打包器并通過 gleam.toml 的[javascript]配置段聲明運行環境見下節。相關配置gleam.toml 的 [javascript] 段與 JavaScript 打包、運行直接相關的配置項定義在 compiler-core/src/config.rs 的JavaScriptConfig中配置項類型默認值說明typescript_declarationsboolfalse是否生成.d.mtsTypeScript 類型聲明文件source_mapsboolfalse是否生成.mjs.map源碼映射文件runtimenode/deno/bun/browser等node目標 JavaScript 運行時deno對象—Deno 運行時的權限配置allow_net、allow_read、allow_env、allow_run、allow_write、allow_ffi、allow_all等default_javascript_runtime函數將默認運行時設定為Runtime::NodeJsconfig.rs。一個典型的配置片段如下示例見 config.rs[javascript] typescript_declarations true runtime node [javascript.deno] allow_net true當typescript_declarations true時編譯器還會生成prelude.d.mts同樣通過include_str!嵌入見 javascript.rs為 prelude 提供類型聲明支撐。可行的優化方向從文檔的記錄方式看這一痛點的理想解法是讓編譯產物 → 可部署 bundle的路徑更直接例如編譯器直接內置打包能力或提供一鍵式打包命令避免用戶自行拼裝 ESM 依賴鏈在輸出層面減少對打包器的隱式要求如將 prelude 內聯進每個模塊消除共享模塊依賴輸出更規范的清單manifest說明模塊間依賴關系方便外部打包工具消費。需要注意的是這些都屬于推斷方向當前倉庫尚未實現真正的進展以 issue 跟蹤器與 CHANGELOG.md 為準。痛點二無法將重復工作轉移到編譯期或初始化期問題描述文檔第二條痛點的原文只有一句示例說明For example, regex compilation例如正則表達式的編譯。含義是在 JavaScript 后端諸如regex.compile(...)這類代價較高的操作如果每次都寫在函數體內那么每次調用都會重新執行而 Gleam 編譯器目前缺少一種機制讓這類結果確定、反復使用的計算提前到編譯期由編譯器完成或初始化期模塊加載時完成一次從而避免運行時重復開銷。為什么難以實現從語言與編譯器結構推斷結合倉庫結構可以從三個層面理解這個限制缺乏編譯期求值/宏機制Gleam 是一門無宏macro-free語言compiler-core/src/ast中只有常量constant.rs、類型typed.rs/untyped.rs等常規 AST 節點沒有面向用戶的編譯期執行通道。用戶無法編寫在編譯時運行一次的代碼常量折疊能力有限雖然存在 compiler-core/src/ast/constant.rs 這樣的常量抽象但constant只支持字面量與純數據構造不包含正則編譯這類需要調用運行時函數的操作模塊初始化順序依賴即使把計算推遲到初始化期JavaScript 后端中 prelude 導入、單例常量如List$Empty$const的求值順序與模塊加載順序javascript.rs 中 imports 先于 statements 輸出也決定了初始化期執行必須在依賴就緒之后這進一步提高了方案設計難度。當前可行的權宜做法在語言層支持之前倉庫中可以看到幾種緩解手段在模塊頂層定義常量Gleam 頂層const會被編譯為模塊級的常量定義module_constant其求值發生在模塊初始化階段而非每次調用天然具備初始化期執行一次的效果使用 FFI 封裝有狀態的預計算對象例如在test/external_only_javascript與test/external_only_erlang這類測試目錄中展示的用法通過external將昂貴的準備步驟放入模塊級的 JS 代碼中執行一次再暴露為 Gleam 函數。值得期待的演進方向文檔把它列為無現成方案的痛點暗示社區未來可能在以下方向探索增加惰性初始化/單例語義讓regex.compile這類調用在模塊首次使用時只執行一次引入編譯期常量求值擴展使純函數的常量折疊能力更強在標準庫層面提供預編譯緩存慣用法。以上方向均屬推測倉庫當前并未實現讀者應避免將其當作既有功能。痛點三JavaScript 自定義類型表示可以更快當前表示方式每個變體一個 class這是三個痛點中源碼證據最充分的一條。Gleam 的 JavaScript 后端將每個自定義類型的每個構造器variant編譯為一個繼承CustomType的 JavaScript class。核心生成邏輯在variant_definition、variant_class_definition、variant_constructor_constant等函數中javascript.rs可以總結為以下產物class 定義如class Wibble extends CustomType { ... }構造函數把各字段賦值到this上帶標簽的字段用this.label無標簽的用this[$index]無字段變體的單例常量Type$Variant$const保證同一變體的所有值共享同一引用從而可以用引用相等做快速比較javascript.rs構造器函數export const Type$Variant (arg1, arg2) new Variant(arg1, arg2)類型判斷函數export const Type$isVariant (value) value instanceof Variant字段訪問函數為每個字段生成Type$Variant$label或Type$Variant$index取值函數并利用TypedCustomType的 accessors 生成跨變體共享字段的 gettershared_custom_type_fields。prelude 中的基類CustomType.withFields負責記錄更新record update它枚舉實例自身屬性用傳入的字段覆蓋后構造新實例見 prelude.mjs。已經存在的性能優化值得說明的是當前實現并非毫無優化。從源碼可以確認兩項已有優化instanceof替代isEqual在模式匹配場景代碼生成器會優先輸出value instanceof Variant而非isEqual(value, new Variant())因為前者無需構造新對象即可完成判斷compiler-core/src/javascript/expression.rs 有明確注釋無字段變體單例化variant_constructor_constant的注釋指出單例常量讓同一變體的所有值共享底層引用從而支持更高效的比較。為什么還可以更快從代碼結構推斷文檔中Would would be optimal?這句反問原文如此表明維護者知道有優化空間但尚未確定什么才是最優表示。從prelude.mjs與生成器代碼可以推斷出若干開銷來源class 實例體積每個帶字段的變體都是一個完整 class 實例對象頭、原型鏈查找與字段賦值都有固定開銷對象屬性訪問 vs 數組索引帶標簽字段通過this.label訪問屬性名查找尤其是動態屬性通常比數組索引慢盡管生成器為無標簽字段生成了this[$index]索引訪問但標簽字段仍走屬性路徑見variant_class_definition中參數與構造體的分支邏輯javascript.rs類型判斷與字段訪問的函數包裝每個變體都生成Type$isVariant、Type$Variant$field等導出函數調用鏈比直接訪問多一層。對照Erlang 后端的表示方式作為參照Erlang 后端采用了完全不同的表示無字段構造器編譯為原子atom帶字段構造器編譯為以原子為標簽的元組如{wibble, Field1, Field2}見 compiler-core/src/erlang.rs 中constructor_atom to_snake_case(constructor.name)與constructor with no fields becomes a regular atom的注釋。元組原子在 Erlang 虛擬機上是高度優化的表示訪問與比較都非常廉價。由此可以推斷JavaScript 后端的理想方向可能是尋找類似緊湊且可快速比較的表示例如基于數組、帶整數標簽的對象或使用Symbol等但正如文檔所述最優方案尚未確定。這些痛點與代碼庫的呼應docs/annoyances.md雖短但三個主題都貫穿于倉庫的各處實現與測試中可作為繼續研究的路標JavaScript 后端測試compiler-core/src/javascript/tests 下包含 prelude、布爾值、位數組等大量快照測試snapshot 測試例如tests/prelude.rs驗證 prelude 導入與Ok/Error的限定名行為任何表示層面的改動都會在此體現自定義類型表示測試JavaScript 測試目錄中的快照文件直接展示了 class、單例常量與instanceof判斷的生成結果是觀察當前表示最直觀的入口打包與運行相關集成測試倉庫中的 test/ 目錄包含external_only_javascript、project_javascript、javascript_prelude等項目級測試如test/javascript_prelude/Makefile與main.mjs驗證編譯產物在真實 JS 運行時下的行為配置解析測試compiler-core/src/config.rs 對應的快照測試如gleam_core__config__*系列鎖定了[javascript]配置段的 JSON/TOML 序列化行為性能基準benchmark/ 下的基準項目如benchmark/list為List等核心數據結構提供基準測試可用于量化表示方案改動帶來的性能變化。結語如何跟進這些痛點docs/annoyances.md是一份活文檔每當維護者在日常編碼中遇到設計層面的不便就可能追加新條目一旦某個痛點有了明確方案并實現它就會從這份清單中移除轉而以功能的形式出現在 CHANGELOG.md 與各版本的變更記錄changelog/中。因此跟蹤這份文檔的變化等同于跟蹤 Gleam 編譯器在 JavaScript 后端與語言能力層面的演進方向。對于想要深入研究或貢獻的讀者建議的閱讀路徑是通讀 docs/annoyances.md理解三個開放問題的語境對照 compiler-core/src/javascript.rs 與 compiler-core/templates/prelude.mjs建立Gleam 源碼 → JS 產物的映射查看 compiler-core/src/javascript/tests 下的快照測試觀察當前生成代碼的具體形態若關注 Erlang 對照可閱讀 compiler-core/src/erlang.rs 中自定義類型的編譯邏輯最終以 issue 跟蹤器與 CHANGELOG.md 為準確認各痛點的解決進度避免把本文描述的方向當作已實現功能。【免費下載鏈接】gleam?? A friendly language for building type-safe, scalable systems!項目地址: https://gitcode.com/GitHub_Trending/gl/gleam創作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考