xbintsc 0.5.52 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +8 -2
  2. package/README.zh-CN.md +9 -3
  3. package/dist/scripts/fetch-tarball.d.ts +28 -0
  4. package/dist/scripts/fetch-tarball.js +57 -0
  5. package/dist/scripts/fetch-tarball.js.map +1 -0
  6. package/dist/scripts/gui-build-options.d.ts +77 -0
  7. package/dist/scripts/gui-build-options.js +134 -0
  8. package/dist/scripts/gui-build-options.js.map +1 -0
  9. package/dist/src/cli/help.d.ts +1 -1
  10. package/dist/src/cli/hints.d.ts +6 -2
  11. package/dist/src/cli/hints.js +16 -2
  12. package/dist/src/cli/hints.js.map +1 -1
  13. package/dist/src/cli/main.js +9 -0
  14. package/dist/src/cli/main.js.map +1 -1
  15. package/dist/src/driver/app-artifacts.d.ts +2 -0
  16. package/dist/src/driver/app-artifacts.js +3 -3
  17. package/dist/src/driver/app-artifacts.js.map +1 -1
  18. package/dist/src/driver/compiler.js +147 -52
  19. package/dist/src/driver/compiler.js.map +1 -1
  20. package/dist/src/driver/icon.d.ts +1 -1
  21. package/dist/src/driver/icon.js +3 -3
  22. package/dist/src/driver/icon.js.map +1 -1
  23. package/dist/src/driver/macos-sdk.d.ts +32 -0
  24. package/dist/src/driver/macos-sdk.js +50 -0
  25. package/dist/src/driver/macos-sdk.js.map +1 -0
  26. package/dist/src/driver/options.d.ts +6 -0
  27. package/dist/src/driver/platform-support.d.ts +45 -0
  28. package/dist/src/driver/platform-support.js +66 -0
  29. package/dist/src/driver/platform-support.js.map +1 -0
  30. package/dist/src/driver/prebuilt.d.ts +26 -0
  31. package/dist/src/driver/prebuilt.js +58 -0
  32. package/dist/src/driver/prebuilt.js.map +1 -0
  33. package/dist/src/driver/runtime-objects.d.ts +11 -0
  34. package/dist/src/driver/runtime-objects.js +14 -5
  35. package/dist/src/driver/runtime-objects.js.map +1 -1
  36. package/dist/src/driver/toolchain-provider.d.ts +8 -0
  37. package/dist/src/driver/toolchain-provider.js +8 -0
  38. package/dist/src/driver/toolchain-provider.js.map +1 -1
  39. package/dist/src/driver/toolchain.d.ts +20 -1
  40. package/dist/src/driver/toolchain.js +19 -0
  41. package/dist/src/driver/toolchain.js.map +1 -1
  42. package/dist/src/driver/version.d.ts +1 -1
  43. package/dist/src/driver/version.js +1 -1
  44. package/dist/src/driver/version.js.map +1 -1
  45. package/dist/src/driver/win-entry.d.ts +1 -1
  46. package/dist/src/driver/win-entry.js +4 -4
  47. package/dist/src/driver/win-entry.js.map +1 -1
  48. package/dist/src/extensions/gui/html.d.ts +10 -7
  49. package/dist/src/extensions/gui/html.js +53 -29
  50. package/dist/src/extensions/gui/html.js.map +1 -1
  51. package/dist/src/extensions/node/child_process/index.d.ts +4 -3
  52. package/dist/src/extensions/node/child_process/index.js +4 -3
  53. package/dist/src/extensions/node/child_process/index.js.map +1 -1
  54. package/dist/tests/cli/hints.test.js +9 -1
  55. package/dist/tests/cli/hints.test.js.map +1 -1
  56. package/dist/tests/cli/main.test.js +9 -0
  57. package/dist/tests/cli/main.test.js.map +1 -1
  58. package/dist/tests/driver/compiler.test.js +107 -0
  59. package/dist/tests/driver/compiler.test.js.map +1 -1
  60. package/dist/tests/driver/cxx-compile-flags.test.d.ts +1 -0
  61. package/dist/tests/driver/cxx-compile-flags.test.js +24 -0
  62. package/dist/tests/driver/cxx-compile-flags.test.js.map +1 -0
  63. package/dist/tests/driver/macos-sdk.test.d.ts +1 -0
  64. package/dist/tests/driver/macos-sdk.test.js +83 -0
  65. package/dist/tests/driver/macos-sdk.test.js.map +1 -0
  66. package/dist/tests/driver/platform-support.test.d.ts +1 -0
  67. package/dist/tests/driver/platform-support.test.js +41 -0
  68. package/dist/tests/driver/platform-support.test.js.map +1 -0
  69. package/dist/tests/driver/prebuilt.test.d.ts +1 -0
  70. package/dist/tests/driver/prebuilt.test.js +44 -0
  71. package/dist/tests/driver/prebuilt.test.js.map +1 -0
  72. package/dist/tests/driver/toolchain-provider.test.js +24 -0
  73. package/dist/tests/driver/toolchain-provider.test.js.map +1 -1
  74. package/dist/tests/driver/toolchain.test.js +23 -0
  75. package/dist/tests/driver/toolchain.test.js.map +1 -1
  76. package/dist/tests/e2e/async-await.test.js +4 -2
  77. package/dist/tests/e2e/async-await.test.js.map +1 -1
  78. package/dist/tests/e2e/compile-node.test.js +21 -0
  79. package/dist/tests/e2e/compile-node.test.js.map +1 -1
  80. package/dist/tests/e2e/fetch.test.js +34 -6
  81. package/dist/tests/e2e/fetch.test.js.map +1 -1
  82. package/dist/tests/e2e/gui-example-pelican3d.test.d.ts +10 -0
  83. package/dist/tests/e2e/gui-example-pelican3d.test.js +94 -0
  84. package/dist/tests/e2e/gui-example-pelican3d.test.js.map +1 -0
  85. package/dist/tests/e2e/gui-script.test.js +48 -0
  86. package/dist/tests/e2e/gui-script.test.js.map +1 -1
  87. package/dist/tests/e2e/unhandled-rejection.test.d.ts +14 -0
  88. package/dist/tests/e2e/unhandled-rejection.test.js +107 -0
  89. package/dist/tests/e2e/unhandled-rejection.test.js.map +1 -0
  90. package/dist/tests/extensions/gui.test.js +44 -0
  91. package/dist/tests/extensions/gui.test.js.map +1 -1
  92. package/dist/tests/scripts/fetch-tarball.test.d.ts +1 -0
  93. package/dist/tests/scripts/fetch-tarball.test.js +70 -0
  94. package/dist/tests/scripts/fetch-tarball.test.js.map +1 -0
  95. package/doc/ai/cli.md +6 -0
  96. package/doc/ai/language-support.md +1 -0
  97. package/doc/ai/troubleshooting.md +32 -2
  98. package/doc/ai/zh-CN/cli.md +5 -0
  99. package/doc/ai/zh-CN/language-support.md +1 -0
  100. package/doc/ai/zh-CN/troubleshooting.md +27 -2
  101. package/doc/gui/build.md +7 -3
  102. package/doc/gui/dom.md +3 -1
  103. package/doc/gui/webgl.md +4 -0
  104. package/doc/gui-scripts.md +33 -17
  105. package/doc/gui.md +17 -1
  106. package/doc/implemented/async.md +1 -0
  107. package/doc/implemented/modules.md +9 -6
  108. package/doc/implemented/node/child-process.md +12 -4
  109. package/doc/implemented/runtime-core.md +1 -1
  110. package/doc/npm-package.md +23 -2
  111. package/doc/post/make-js-great-again.md +30 -0
  112. package/doc/requirements.md +31 -0
  113. package/doc/unimplemented/deviations.md +1 -0
  114. package/doc/zh-CN/gui/build.md +4 -2
  115. package/doc/zh-CN/gui/dom.md +3 -1
  116. package/doc/zh-CN/gui/webgl.md +1 -0
  117. package/doc/zh-CN/gui-scripts.md +23 -11
  118. package/doc/zh-CN/gui.md +11 -1
  119. package/doc/zh-CN/implemented/async.md +1 -0
  120. package/doc/zh-CN/implemented/modules.md +8 -4
  121. package/doc/zh-CN/implemented/node/child-process.md +10 -3
  122. package/doc/zh-CN/implemented/runtime-core.md +1 -1
  123. package/doc/zh-CN/npm-package.md +18 -1
  124. package/doc/zh-CN/post/make-js-great-again.md +32 -0
  125. package/doc/zh-CN/requirements.md +25 -0
  126. package/doc/zh-CN/unimplemented/deviations.md +1 -0
  127. package/package.json +7 -7
  128. package/runtime/ext_node/child_process/child_process.c +149 -7
  129. package/runtime/rt.h +4 -0
  130. package/runtime/xt_async.c +4 -0
  131. package/runtime/xt_generator.c +2 -0
  132. package/runtime/xt_loop.c +8 -4
  133. package/runtime/xt_promise.c +103 -0
  134. package/scripts/build-gui.ts +17 -5
  135. package/scripts/build-media.ts +89 -10
  136. package/scripts/build-runtime.ts +7 -2
  137. package/scripts/fetch-tarball.ts +71 -0
  138. package/scripts/gui-build-cache.ts +28 -0
  139. package/scripts/gui-build-options.ts +26 -2
  140. package/scripts/package-release.ts +8 -0
  141. package/src/cli/hints.ts +19 -2
  142. package/src/cli/main.ts +11 -0
  143. package/src/driver/app-artifacts.ts +5 -3
  144. package/src/driver/compiler.ts +167 -56
  145. package/src/driver/icon.ts +3 -2
  146. package/src/driver/macos-sdk.ts +52 -0
  147. package/src/driver/options.ts +6 -0
  148. package/src/driver/platform-support.ts +69 -0
  149. package/src/driver/prebuilt.ts +61 -0
  150. package/src/driver/runtime-objects.ts +26 -5
  151. package/src/driver/toolchain-provider.ts +11 -0
  152. package/src/driver/toolchain.ts +26 -1
  153. package/src/driver/version.ts +1 -1
  154. package/src/driver/win-entry.ts +4 -3
  155. package/src/extensions/gui/html.ts +61 -31
  156. package/src/extensions/node/child_process/index.ts +4 -3
@@ -0,0 +1,32 @@
1
+ # 这可能是今年最让JS开发者激动的事情了
2
+
3
+ ![3D鹈鹕骑自行车](../../../examples/gui/pelican-3d/preview.png)
4
+
5
+ 上面是我用JS+HTML+WebGL2做的鹈鹕3D骑自行车,FPS也非常不错。
6
+
7
+ 你可能会说“这不是用Ai有手就行了么?”
8
+
9
+ 但是,如果我说我把这上面用xbintsc编译成原生二进制,还不到10MB,还能在没有WebView环境上跑,还能支持Node.js和HTML混写,如下:
10
+
11
+ ```html
12
+ <html>
13
+ <body>
14
+ <div id="out">loading...</div>
15
+ <script lang="ts">
16
+ import { readFileSync } from "fs";
17
+
18
+ const out = document.getElementById("out");
19
+ const world = readFileSync("data.txt", "utf8");
20
+ out.textContent = "hello " + world;
21
+ </script>
22
+ </body>
23
+ </html>
24
+ ```
25
+
26
+ 要知道以前光chrome最少也得有100MB,用xbintsc编译产物10MB还不需要WebView环境,这可不仅仅代表桌面端开发体积极大减小和更加正确的实现路径的问题。
27
+
28
+ 更加具有划时代意义的是,JS开发者在嵌入式设备跑你想要做的应用成为了可能。
29
+
30
+ [xbintsc开源地址](https://github.com/zy445566/xbintsc)
31
+
32
+
@@ -14,6 +14,27 @@ xbintsc 不再自带编译器,因此每个平台都需要安装一个兼容 cl
14
14
  如果通过 npm 安装编译器而不是使用发布压缩包,Node.js 22+ 会直接运行编译器本身;
15
15
  见[通过 npm 包使用 xbintsc](./npm-package.md)。
16
16
 
17
+ ## 支持的平台
18
+
19
+ 发布二进制和预编译运行库归档面向 xbintsc 支持的最低平台版本。CI 使用**各宿主仍
20
+ 能产出发布包的最老工具链**构建它们,因此你机器上更新的 Xcode 或 Visual Studio 可以链接
21
+ 这些产物;反过来(旧工具链链接新产物)才会出现 `LNK2038`、`Undefined symbols` 或
22
+ `was built for newer macOS version`。
23
+
24
+ | 平台 | 生成的二进制可运行于 | 构建基线(CI) |
25
+ | --- | --- | --- |
26
+ | macOS x64 / arm64 | **macOS 11.0**(Big Sur)及以上 | 镜像中最老的 clang 16+ Xcode,`MACOSX_DEPLOYMENT_TARGET=11.0` |
27
+ | Windows x64 | **Windows 11** 及以上 | `windows-2025` 镜像自带的工具链(当前为 VS 2026) |
28
+ | Windows arm64 | **Windows 11** | `windows-11-arm` 镜像自带的工具链(当前为 VS 2026;没有更老的 arm64 镜像) |
29
+ | Linux x64 / arm64 | **glibc 2.39**(Ubuntu 24.04)及以上 | `ubuntu-24.04` 上的 clang 16 + lld 16 |
30
+
31
+ 编译器工具链下限为:macOS 上的 Xcode 16(Apple clang 16)、Windows 上 `windows-2025`
32
+ 镜像自带的 Visual Studio(当前为 VS 2026)、Linux 上的 clang 16。因此构建机的要求比生成二进制的
33
+ 运行要求更高:Xcode 16 本身需要 macOS 14.5+,macOS 11–14 的构建机需要另外安装
34
+ LLVM 16+(例如 Homebrew 的 `llvm`)。`xbintsc doctor` 会打印当前宿主的支持下限。如果归档是用比本机更新的工具链构建的,`build` 会自动改用随包 C 源码
35
+ 重试(见 [npm 包指南](./npm-package.md#首次构建与缓存));随包的 `gui.a`/`media.a`
36
+ 无法这样重编,因为 SDL3/FFmpeg 的源码不在包里。
37
+
17
38
  clang 15 及更旧版本会在任何程序的第一个字符串字面量处失败:
18
39
 
19
40
  ```
@@ -104,6 +125,10 @@ xcode-select -p # 打印当前开发者目录
104
125
  xcrun --show-sdk-path # 打印 SDK 路径
105
126
  ```
106
127
 
128
+ xbintsc 会自己执行第二条命令,并把结果以 `-isysroot` 传给 clang,因此即使 clang
129
+ 是用绝对路径选中的(CI 的基线 Xcode,或 `xbintsc_CLANG`),也仍能找到 SDK 里的
130
+ C 头文件与链接桩。自己设置 `SDKROOT` 优先,可用它固定某个 SDK。
131
+
107
132
  ## Linux —— 安装 clang 与 lld
108
133
 
109
134
  常规的 glibc 发行版加上 `clang`、`lld` 即可:
@@ -16,6 +16,7 @@
16
16
  | 内存管理 | 非移动标记-清扫回收器(在 `xt_alloc` 之后):显式根、子系统根提供者与保守 C 栈扫描。单线程、stop-the-world;无移动/压缩与分代 |
17
17
  | 函数 `arity` / 调用参数个数 | 对本地可见函数的直接调用由默认[语义检查](../implemented/checker.md)校验(`TS4004`);通过属性、导入、展开实参或重新赋值后的绑定发起的调用不校验。`fn.length` 报告声明的 arity |
18
18
  | `async` / `await` | 已在栈式协程上真实挂起;`await` 总会排入一次微任务(非 Promise 与已 settle 的 Promise 也一样),因此与 pending `.then` 回调的交错顺序符合规范。顶层 `await` 会阻塞事件循环等待,而不是让出模块主体。`fs/promises` 与 `fetch` 把阻塞工作 offload 到 4 线程池并在 reactor 上 settle。异步生成器可在 `await` 与 `yield` 处挂起,`for await...of` 每次迭代等待一步 |
19
+ | 未处理的 rejection | 在程序结束时才报告,而不是在当轮 tick 结束时:无人观察的 rejection 会在事件循环排空后打印并使进程以状态码 1 退出,因此长期运行的进程(如服务端)会一直运行到退出,稍后补挂的处理函数也仍会抑制该报告。Node 在微任务队列排空后立即抛出致命错误 |
19
20
  | 异步生成器清理 | `for await...of` 提前退出时不会调用迭代器的 `return()`。异步生成器中的 `yield*` 只委托同步可迭代对象。在挂起的 `yield` 处调用 `.return()` 会直接完成而不执行 `finally`(与同步生成器相同的限制) |
20
21
  | `super` | `super.x` / `super[x]` 从当前执行类的父级持有者读取(实例方法中是父原型,`static` 方法或静态块中是父构造函数),因此任意继承深度都正确 |
21
22
  | `Error.stack` | 仅函数级帧:每帧为 `at 名称 (文件:行)`,行号取自函数声明(最内层帧会更新为错误构造处);没有列号,生成器 / `async` 函数体不产生帧(它们会切换栈) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xbintsc",
3
- "version": "0.5.52",
3
+ "version": "0.6.8",
4
4
  "description": "xbintsc - a TypeScript binary compiler (TypeScript -> LLVM IR -> native binary)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -72,12 +72,12 @@
72
72
  "vitest": "^2.1.8"
73
73
  },
74
74
  "optionalDependencies": {
75
- "@xbintsc/linux-x64": "0.5.52",
76
- "@xbintsc/linux-arm64": "0.5.52",
77
- "@xbintsc/darwin-x64": "0.5.52",
78
- "@xbintsc/darwin-arm64": "0.5.52",
79
- "@xbintsc/win32-x64": "0.5.52",
80
- "@xbintsc/win32-arm64": "0.5.52"
75
+ "@xbintsc/linux-x64": "0.6.8",
76
+ "@xbintsc/linux-arm64": "0.6.8",
77
+ "@xbintsc/darwin-x64": "0.6.8",
78
+ "@xbintsc/darwin-arm64": "0.6.8",
79
+ "@xbintsc/win32-x64": "0.6.8",
80
+ "@xbintsc/win32-arm64": "0.6.8"
81
81
  },
82
82
  "engines": {
83
83
  "node": ">=22"
@@ -7,9 +7,10 @@
7
7
  * const result = spawnSync("clang", ["--version"], { cwd, encoding: "utf8" });
8
8
  * result.status / result.stdout / result.stderr
9
9
  *
10
- * `options.cwd` is honoured; stdout/stderr are always captured as UTF-8
11
- * strings. On Windows the process is run with `_spawnvp` and redirected
12
- * through temporary files (which also avoids pipe-buffer deadlocks).
10
+ * `options.cwd` and `options.env` are honoured (the latter with Node's replace
11
+ * semantics); stdout/stderr are always captured as UTF-8 strings. On Windows
12
+ * the child runs under `CreateProcess` with anonymous pipes drained by reader
13
+ * threads (which also avoids pipe-buffer deadlocks).
13
14
  */
14
15
 
15
16
  #include "rt.h"
@@ -60,6 +61,58 @@ static int option_stdio_inherit(xt_value options) {
60
61
  return stdio && XT_IS_STRING(stdio) && strcmp(xt_string_data(stdio), "inherit") == 0;
61
62
  }
62
63
 
64
+ /*
65
+ * `options.env` replaces the child's environment, exactly like Node: an absent
66
+ * option inherits the parent's environment, while a present one is the complete
67
+ * environment (spread `process.env` into it to extend rather than replace).
68
+ *
69
+ * The runtime used to ignore this option, which made every `spawnSync` inherit:
70
+ * a caller passing `SDKROOT` or `INCLUDE` silently got nothing, and a
71
+ * self-hosted compiler could not hand the macOS SDK to clang.
72
+ */
73
+
74
+ /** The `env` object, or `XT_UNDEFINED` when the child should inherit. */
75
+ static xt_value option_env(xt_value options) {
76
+ if (!XT_IS_OBJECT(options)) return XT_UNDEFINED;
77
+ xt_value env = xt_object_get(options, xt_string_from_cstr("env"));
78
+ return XT_IS_OBJECT(env) ? env : XT_UNDEFINED;
79
+ }
80
+
81
+ /** The number of `KEY=VALUE` entries the environment object provides. */
82
+ static int32_t env_size(xt_value env) {
83
+ xt_value keys = xt_object_keys(env);
84
+ return XT_IS_ARRAY(keys) ? xt_array_size(keys) : 0;
85
+ }
86
+
87
+ static void free_env_entries(char **entries, int32_t count) {
88
+ if (entries == NULL) return;
89
+ for (int32_t i = 0; i < count; i++) free(entries[i]);
90
+ free(entries);
91
+ }
92
+
93
+ /** `KEY=VALUE` entries for the environment object, in object key order. */
94
+ static char **env_entries(xt_value env, int32_t count) {
95
+ char **entries = (char **)calloc((size_t)(count > 0 ? count : 1), sizeof(char *));
96
+ if (entries == NULL) return NULL;
97
+ xt_value keys = xt_object_keys(env);
98
+ xt_value *items = XT_IS_ARRAY(keys) ? xt_array_items(keys) : NULL;
99
+ for (int32_t i = 0; i < count; i++) {
100
+ xt_value key = items[i];
101
+ xt_value raw = xt_object_get(env, key);
102
+ /* Node ignores `undefined` values instead of passing "undefined". */
103
+ if (XT_IS_UNDEFINED(raw)) continue;
104
+ const char *name = xt_string_data(xt_to_string(key));
105
+ const char *value = xt_string_data(xt_to_string(raw));
106
+ if (name == NULL || value == NULL) continue;
107
+ size_t length = strlen(name) + strlen(value) + 2;
108
+ char *entry = (char *)malloc(length);
109
+ if (entry == NULL) continue;
110
+ snprintf(entry, length, "%s=%s", name, value);
111
+ entries[i] = entry;
112
+ }
113
+ return entries;
114
+ }
115
+
63
116
  static xt_value make_result(double status, const char *out, const char *err) {
64
117
  xt_value result = xt_object_new();
65
118
  xt_set(result, xt_string_from_cstr("status"), xt_number(status));
@@ -181,18 +234,60 @@ static char *build_command_line(char **child_argv, int count) {
181
234
  return line;
182
235
  }
183
236
 
237
+ /* `CreateProcessA` wants one ANSI block, entries sorted case-insensitively. */
238
+ static int compare_env_entries(const void *left, const void *right) {
239
+ const char *a = *(const char *const *)left;
240
+ const char *b = *(const char *const *)right;
241
+ if (a == NULL) return b == NULL ? 0 : 1;
242
+ if (b == NULL) return -1;
243
+ return _stricmp(a, b);
244
+ }
245
+
246
+ /** The environment block for `CreateProcessA`, or NULL to inherit. */
247
+ static char *build_env_block(xt_value env) {
248
+ if (!XT_IS_OBJECT(env)) return NULL;
249
+ int32_t count = env_size(env);
250
+ char **entries = env_entries(env, count);
251
+ if (entries == NULL) return NULL;
252
+ qsort(entries, (size_t)count, sizeof(char *), compare_env_entries);
253
+ size_t total = 1;
254
+ for (int32_t i = 0; i < count; i++) total += entries[i] != NULL ? strlen(entries[i]) + 1 : 0;
255
+ char *block = (char *)malloc(total + 1);
256
+ if (block == NULL) {
257
+ free_env_entries(entries, count);
258
+ return NULL;
259
+ }
260
+ char *cursor = block;
261
+ for (int32_t i = 0; i < count; i++) {
262
+ if (entries[i] == NULL) continue;
263
+ size_t length = strlen(entries[i]) + 1;
264
+ memcpy(cursor, entries[i], length);
265
+ cursor += length;
266
+ }
267
+ /* The block ends with an extra terminator; an empty environment is "\0\0". */
268
+ *cursor++ = 0;
269
+ *cursor = 0;
270
+ free_env_entries(entries, count);
271
+ return block;
272
+ }
273
+
184
274
  xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
185
275
  const char *command = argc > 0 ? xt_string_data(xt_to_string(argv[0])) : NULL;
186
276
  if (!command) command = "";
187
277
  xt_value args = argc > 1 ? argv[1] : XT_UNDEFINED;
188
278
  const char *cwd = argc > 2 ? option_cwd(argv[2]) : NULL;
279
+ char *env_block = build_env_block(option_env(argc > 2 ? argv[2] : XT_UNDEFINED));
189
280
 
190
281
  int count = 0;
191
282
  char **child_argv = (char **)build_argv(command, args, &count);
192
- if (!child_argv) return make_result(-1, "", "out of memory");
283
+ if (!child_argv) {
284
+ free(env_block);
285
+ return make_result(-1, "", "out of memory");
286
+ }
193
287
  char *command_line = build_command_line(child_argv, count);
194
288
  if (!command_line) {
195
289
  free(child_argv);
290
+ free(env_block);
196
291
  return make_result(-1, "", "out of memory");
197
292
  }
198
293
 
@@ -204,9 +299,10 @@ xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
204
299
 
205
300
  if (option_stdio_inherit(argc > 2 ? argv[2] : XT_UNDEFINED)) {
206
301
  BOOL created = CreateProcessA(
207
- NULL, command_line, NULL, NULL, TRUE, 0, NULL, cwd, &startup, &process);
302
+ NULL, command_line, NULL, NULL, TRUE, 0, env_block, cwd, &startup, &process);
208
303
  free(command_line);
209
304
  free(child_argv);
305
+ free(env_block);
210
306
  if (!created) return make_result(-1, "", "failed to start process");
211
307
  WaitForSingleObject(process.hProcess, INFINITE);
212
308
  DWORD exit_code = 0;
@@ -229,6 +325,7 @@ xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
229
325
  close_capture_pipe(&err_pipe);
230
326
  free(command_line);
231
327
  free(child_argv);
328
+ free(env_block);
232
329
  return make_result(-1, "", "could not create pipes");
233
330
  }
234
331
  startup.dwFlags = STARTF_USESTDHANDLES;
@@ -237,9 +334,10 @@ xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
237
334
  startup.hStdError = err_write;
238
335
 
239
336
  BOOL created = CreateProcessA(
240
- NULL, command_line, NULL, NULL, TRUE, 0, NULL, cwd, &startup, &process);
337
+ NULL, command_line, NULL, NULL, TRUE, 0, env_block, cwd, &startup, &process);
241
338
  free(command_line);
242
339
  free(child_argv);
340
+ free(env_block);
243
341
  CloseHandle(out_write);
244
342
  CloseHandle(err_write);
245
343
  if (!created) {
@@ -285,30 +383,68 @@ static void append_chunk(char **buffer, size_t *length, size_t *capacity, const
285
383
  (*buffer)[*length] = 0;
286
384
  }
287
385
 
386
+ /** A NULL-terminated `envp` for `exec`, or NULL when the child inherits. */
387
+ static char **build_envp(xt_value env) {
388
+ if (!XT_IS_OBJECT(env)) return NULL;
389
+ int32_t count = env_size(env);
390
+ char **entries = env_entries(env, count);
391
+ if (entries == NULL) return NULL;
392
+ char **envp = (char **)calloc((size_t)count + 1, sizeof(char *));
393
+ if (envp == NULL) {
394
+ free_env_entries(entries, count);
395
+ return NULL;
396
+ }
397
+ int32_t kept = 0;
398
+ for (int32_t i = 0; i < count; i++) {
399
+ if (entries[i] != NULL) envp[kept++] = entries[i];
400
+ }
401
+ envp[kept] = NULL;
402
+ /* The strings now belong to `envp`; only the index array is released here. */
403
+ free(entries);
404
+ return envp;
405
+ }
406
+
407
+ /** Release an `envp` built by {@link build_envp}. */
408
+ static void free_envp(char **envp) {
409
+ if (envp == NULL) return;
410
+ for (char **entry = envp; *entry != NULL; entry++) free(*entry);
411
+ free(envp);
412
+ }
413
+
288
414
  xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
289
415
  const char *command = argc > 0 ? xt_string_data(xt_to_string(argv[0])) : NULL;
290
416
  if (!command) command = "";
291
417
  xt_value args = argc > 1 ? argv[1] : XT_UNDEFINED;
292
418
  const char *cwd = argc > 2 ? option_cwd(argv[2]) : NULL;
419
+ char **envp = build_envp(option_env(argc > 2 ? argv[2] : XT_UNDEFINED));
293
420
 
294
421
  int count = 0;
295
422
  char **child_argv = (char **)build_argv(command, args, &count);
296
- if (!child_argv) return make_result(-1, "", "out of memory");
423
+ if (!child_argv) {
424
+ free_envp(envp);
425
+ return make_result(-1, "", "out of memory");
426
+ }
297
427
 
298
428
  if (option_stdio_inherit(argc > 2 ? argv[2] : XT_UNDEFINED)) {
299
429
  pid_t inherit_pid = fork();
300
430
  if (inherit_pid < 0) {
301
431
  free(child_argv);
432
+ free_envp(envp);
302
433
  return make_result(-1, "", "failed to fork");
303
434
  }
304
435
  if (inherit_pid == 0) {
305
436
  if (cwd) chdir(cwd);
437
+ if (envp != NULL) {
438
+ extern char **environ;
439
+ environ = envp;
440
+ }
306
441
  execvp(command, child_argv);
307
442
  _exit(127);
308
443
  }
309
444
  int inherit_status = 0;
310
445
  waitpid(inherit_pid, &inherit_status, 0);
311
446
  free(child_argv);
447
+ free_envp(envp);
312
448
  return make_result(
313
449
  WIFEXITED(inherit_status) ? (double)WEXITSTATUS(inherit_status) : -1.0, "", "");
314
450
  }
@@ -317,6 +453,7 @@ xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
317
453
  int err_pipe[2];
318
454
  if (pipe(out_pipe) != 0 || pipe(err_pipe) != 0) {
319
455
  free(child_argv);
456
+ free_envp(envp);
320
457
  return make_result(-1, "", "could not create pipes");
321
458
  }
322
459
 
@@ -329,6 +466,10 @@ xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
329
466
  close(err_pipe[0]);
330
467
  close(err_pipe[1]);
331
468
  if (cwd) chdir(cwd);
469
+ if (envp != NULL) {
470
+ extern char **environ;
471
+ environ = envp;
472
+ }
332
473
  execvp(command, child_argv);
333
474
  _exit(127);
334
475
  }
@@ -371,6 +512,7 @@ xt_value xt_child_process_spawn_sync(int32_t argc, xt_value *argv) {
371
512
  free(buffers[0]);
372
513
  free(buffers[1]);
373
514
  free(child_argv);
515
+ free_envp(envp);
374
516
  return result;
375
517
  }
376
518
 
package/runtime/rt.h CHANGED
@@ -334,6 +334,10 @@ xt_value xt_promise_static(xt_value name, int32_t argc, xt_value *argv);
334
334
  xt_value xt_await(xt_value value);
335
335
  /** Run queued promise reactions; called once the program body finishes. */
336
336
  void xt_drain_microtasks(void);
337
+ /** Mark `value` (a promise) as observed, so its rejection is not reported. */
338
+ void xt_promise_observe_value(xt_value value);
339
+ /** Print rejected promises that no handler ever observed; returns how many. */
340
+ int xt_promise_report_unhandled(void);
337
341
 
338
342
  /* -- host event loop ------------------------------------------------------ */
339
343
  /* A small select(2) reactor for host extensions (node's net/dgram/http). The
@@ -122,6 +122,10 @@ void xt_async_wake(void *asyncPointer, xt_value value, int rejected) {
122
122
  * queues a microtask before resuming, so an await always interleaves with
123
123
  * pending `.then` callbacks exactly like the specification. */
124
124
  xt_value xt_await(xt_value value) {
125
+ /* The awaiting code observes the outcome, so a rejection here is handled by
126
+ * definition — it is rethrown inside the coroutine, and the promise of the
127
+ * enclosing `async` function is what gets reported if nobody catches it. */
128
+ xt_promise_observe_value(value);
125
129
  /* An `await` inside an async generator suspends that generator's coroutine
126
130
  * while leaving the current `.next()` promise pending. */
127
131
  xt_value generatorResult;
@@ -395,6 +395,8 @@ int xt_generator_await(xt_value value, xt_value *result) {
395
395
  /* A nested async function inside the generator runs on its own coroutine;
396
396
  * its awaits must not suspend the generator. */
397
397
  if (!xt_coroutine_is_current(gen->coroutine)) return 0;
398
+ /* The generator's body observes the outcome, so this rejection is handled. */
399
+ xt_promise_observe_value(value);
398
400
  int state = xt_promise_state_of(value);
399
401
  if (state == XT_PROMISE_PENDING) {
400
402
  gen->await_threw = 0;
package/runtime/xt_loop.c CHANGED
@@ -449,11 +449,15 @@ void xt_run_event_loop(void) {
449
449
  responsible for calling `xt_loop_poll` while it runs. */
450
450
  if (xt_loop_main != NULL) {
451
451
  xt_loop_main();
452
- return;
453
- }
454
- while (xt_loop_has_work()) {
455
- if (xt_loop_poll(-1) < 0) break;
452
+ } else {
453
+ while (xt_loop_has_work()) {
454
+ if (xt_loop_poll(-1) < 0) break;
455
+ }
456
456
  }
457
+ /* A rejection that no handler ever observed is fatal, like Node's default
458
+ * `--unhandled-rejections=throw`. Without this an `async main()` that throws
459
+ * exits 0 with no output at all, which hides the failure completely. */
460
+ if (xt_promise_report_unhandled() > 0) exit(1);
457
461
  }
458
462
 
459
463
  int xt_loop_has_work(void) {
@@ -11,6 +11,7 @@
11
11
 
12
12
  #include "rt_internal.h"
13
13
 
14
+ #include <stdio.h>
14
15
  #include <stdlib.h>
15
16
  #include <string.h>
16
17
 
@@ -33,6 +34,12 @@ struct xt_promise {
33
34
  xt_reaction **reactions;
34
35
  uint32_t reaction_count;
35
36
  uint32_t reaction_capacity;
37
+ /* Unhandled-rejection tracking. `observed` is set once something can see
38
+ * this promise's outcome: a `.then`/`.catch` registration, an `await`, or
39
+ * adoption by another promise. `report_index` is its slot in `xt_unhandled`
40
+ * (`XT_PROMISE_UNLISTED` while it is not queued). */
41
+ int observed;
42
+ uint32_t report_index;
36
43
  };
37
44
 
38
45
  /* -- microtask queue ------------------------------------------------------ */
@@ -64,6 +71,58 @@ static xt_value xt_promise_value(xt_promise *promise) {
64
71
  return XT_FROM_PTR(XT_TAG_OBJECT, promise);
65
72
  }
66
73
 
74
+ /* -- unhandled rejections ------------------------------------------------- */
75
+ /*
76
+ * A promise that rejects without anybody observing the outcome is a bug, and
77
+ * an invisible one here: a compiled program's entry point is usually `async`,
78
+ * so an escaping exception rejects *its* promise and nothing else looks at it.
79
+ * The process used to print nothing and exit 0, which hides the failure.
80
+ *
81
+ * A rejected promise joins this queue and leaves it the moment a handler (or
82
+ * an `await`) is registered — including one attached later, from a timer. What
83
+ * is still queued once the event loop drains is reported on stderr and the
84
+ * process exits non-zero, like Node's default `unhandled-rejections=throw`.
85
+ */
86
+ #define XT_PROMISE_UNLISTED 0xFFFFFFFFu
87
+
88
+ static xt_promise **xt_unhandled = NULL;
89
+ static uint32_t xt_unhandled_count = 0;
90
+ static uint32_t xt_unhandled_capacity = 0;
91
+
92
+ static void xt_unhandled_remove(xt_promise *promise) {
93
+ uint32_t index = promise->report_index;
94
+ if (index == XT_PROMISE_UNLISTED) return;
95
+ uint32_t last = xt_unhandled_count - 1;
96
+ if (index != last) {
97
+ xt_promise *moved = xt_unhandled[last];
98
+ xt_unhandled[index] = moved;
99
+ moved->report_index = index;
100
+ }
101
+ xt_unhandled_count = last;
102
+ promise->report_index = XT_PROMISE_UNLISTED;
103
+ }
104
+
105
+ static void xt_unhandled_add(xt_promise *promise) {
106
+ if (promise->observed || promise->report_index != XT_PROMISE_UNLISTED) return;
107
+ if (xt_unhandled_count >= xt_unhandled_capacity) {
108
+ xt_unhandled_capacity = xt_unhandled_capacity == 0 ? 8 : xt_unhandled_capacity * 2;
109
+ xt_unhandled = (xt_promise **)realloc(xt_unhandled, sizeof(xt_promise *) * xt_unhandled_capacity);
110
+ if (!xt_unhandled) abort();
111
+ }
112
+ promise->report_index = xt_unhandled_count;
113
+ xt_unhandled[xt_unhandled_count++] = promise;
114
+ }
115
+
116
+ /* Something is listening now, so this promise can never be "unhandled". */
117
+ static void xt_promise_observe(xt_promise *promise) {
118
+ if (!promise) return;
119
+ promise->observed = 1;
120
+ xt_unhandled_remove(promise);
121
+ }
122
+
123
+ /** Mark `value` (when it is a promise) as observed; see `xt_promise_observe`. */
124
+ void xt_promise_observe_value(xt_value value) { xt_promise_observe(xt_as_promise(value)); }
125
+
67
126
  static void xt_promise_gc_register(void);
68
127
 
69
128
  static xt_promise *xt_promise_new(void) {
@@ -73,6 +132,8 @@ static xt_promise *xt_promise_new(void) {
73
132
  promise->reactions = NULL;
74
133
  promise->reaction_count = 0;
75
134
  promise->reaction_capacity = 0;
135
+ promise->observed = 0;
136
+ promise->report_index = XT_PROMISE_UNLISTED;
76
137
  xt_promise_gc_register();
77
138
  return promise;
78
139
  }
@@ -90,6 +151,9 @@ static void xt_promise_gc_scan_roots(void) {
90
151
  xt_gc_mark_header((xt_header *)reaction->result);
91
152
  if (reaction->async) xt_gc_mark_header((xt_header *)reaction->async);
92
153
  }
154
+ /* Pending rejections are reported by identity, and the reason must survive
155
+ * long enough to be printed. */
156
+ for (uint32_t i = 0; i < xt_unhandled_count; i++) xt_gc_mark_header((xt_header *)xt_unhandled[i]);
93
157
  }
94
158
 
95
159
  static void xt_promise_gc_register(void) {
@@ -115,6 +179,8 @@ void xt_promise_gc_trace(xt_header *header) {
115
179
 
116
180
  void xt_promise_gc_release(xt_header *header) {
117
181
  xt_promise *promise = (xt_promise *)header;
182
+ /* A listed rejection is a GC root, so this is only a safety net. */
183
+ xt_unhandled_remove(promise);
118
184
  /* Pending reactions are owned solely by the promise; queued ones live in
119
185
  * `xt_microtasks` and are freed as they run. */
120
186
  for (uint32_t i = 0; i < promise->reaction_count; i++) free(promise->reactions[i]);
@@ -129,6 +195,9 @@ static void xt_promise_resolve_into(xt_promise *result, xt_value value) {
129
195
  xt_promise_settle(result, XT_PROMISE_FULFILLED, value);
130
196
  return;
131
197
  }
198
+ /* `result` follows `other`, so `other`'s rejection is consumed here even if
199
+ * `result` ends up unhandled itself (Node reports only the outermost). */
200
+ xt_promise_observe(other);
132
201
  if (other->state == XT_PROMISE_PENDING) {
133
202
  xt_reaction *reaction = (xt_reaction *)malloc(sizeof(xt_reaction));
134
203
  if (!reaction) abort();
@@ -152,6 +221,7 @@ static void xt_promise_settle(xt_promise *promise, int state, xt_value value) {
152
221
  if (promise->state != XT_PROMISE_PENDING) return;
153
222
  promise->state = state;
154
223
  promise->value = value;
224
+ if (state == XT_PROMISE_REJECTED) xt_unhandled_add(promise);
155
225
  for (uint32_t i = 0; i < promise->reaction_count; i++) xt_microtask_push(promise->reactions[i]);
156
226
  promise->reaction_count = 0;
157
227
  }
@@ -206,6 +276,8 @@ void xt_promise_queue_async_wake(void *asyncPointer, xt_value settled, int rejec
206
276
  xt_promise *promise = xt_promise_new();
207
277
  promise->state = rejected ? XT_PROMISE_REJECTED : XT_PROMISE_FULFILLED;
208
278
  promise->value = settled;
279
+ /* An internal handoff to a coroutine, never a rejection anyone can see. */
280
+ promise->observed = 1;
209
281
  xt_reaction *reaction = (xt_reaction *)malloc(sizeof(xt_reaction));
210
282
  if (!reaction) abort();
211
283
  reaction->promise = promise;
@@ -239,6 +311,8 @@ xt_value xt_promise_value_of(xt_value value) {
239
311
  void xt_promise_add_async_waiter(xt_value promiseValue, void *async) {
240
312
  xt_promise *promise = xt_as_promise(promiseValue);
241
313
  if (!promise || promise->state != XT_PROMISE_PENDING) return;
314
+ /* The coroutine sees the rejection, so this promise is handled. */
315
+ xt_promise_observe(promise);
242
316
  xt_reaction *reaction = (xt_reaction *)malloc(sizeof(xt_reaction));
243
317
  if (!reaction) abort();
244
318
  reaction->promise = promise;
@@ -346,6 +420,9 @@ xt_value xt_promise_static(xt_value name, int32_t argc, xt_value *argv) {
346
420
  for (uint32_t i = 0; i < array->length; i++) {
347
421
  xt_value item = array->items[i];
348
422
  xt_promise *inner = xt_is_promise(item) ? xt_as_promise(item) : NULL;
423
+ /* The combinator reads every element's outcome, including a rejection
424
+ * it forwards to its own result. */
425
+ xt_promise_observe(inner);
349
426
  int rejected = inner && inner->state == XT_PROMISE_REJECTED;
350
427
  if (strcmp(fn, "allSettled") == 0) {
351
428
  xt_value record = xt_object_new();
@@ -400,6 +477,10 @@ xt_value xt_promise_method(xt_value target, const char *method, int32_t argc, xt
400
477
  return XT_UNDEFINED;
401
478
  }
402
479
  if (strcmp(method, "then") == 0) {
480
+ /* `.then` with no rejection handler still consumes this promise's
481
+ * rejection: the derivative carries it on and is reported if nobody
482
+ * handles *that* one, exactly like Node. */
483
+ xt_promise_observe(promise);
403
484
  xt_promise *result = xt_promise_new();
404
485
  xt_reaction *reaction = (xt_reaction *)malloc(sizeof(xt_reaction));
405
486
  if (!reaction) abort();
@@ -448,3 +529,25 @@ xt_value xt_queue_microtask(int32_t argc, xt_value *argv) {
448
529
  xt_promise_method(promise, "then", 1, &callback, &handled);
449
530
  return XT_UNDEFINED;
450
531
  }
532
+
533
+ /* -- reporting ------------------------------------------------------------ */
534
+
535
+ int xt_promise_report_unhandled(void) {
536
+ int reported = 0;
537
+ for (uint32_t i = 0; i < xt_unhandled_count; i++) {
538
+ xt_promise *promise = xt_unhandled[i];
539
+ if (!promise || promise->observed) continue;
540
+ reported++;
541
+ /* `Error` (and its subclasses) carry a captured stack; everything else is
542
+ * stringified the way Node prints a rejection reason. */
543
+ xt_value text = xt_error_stack(promise->value);
544
+ if (text == XT_UNDEFINED) text = xt_to_string(promise->value);
545
+ xt_string *bytes = xt_as_string(text);
546
+ if (bytes) {
547
+ fprintf(stderr, "Unhandled promise rejection\n%s\n", xt_string_bytes(bytes));
548
+ } else {
549
+ fprintf(stderr, "Unhandled promise rejection\n");
550
+ }
551
+ }
552
+ return reported;
553
+ }