@sproutboat/toolchain 0.4.17 → 0.4.19

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.19
4
+
5
+ ### Patch Changes
6
+
7
+ - 872198d: Standalone binaries accept HTTP/1.0 requests instead of answering 505 (baronunread/sproutboat#236), so they work behind nginx's default `proxy_http_version 1.0`. uWebSockets only parsed `HTTP/1.1` request lines. A helper now edits its `HttpParser.h` before each native-fetch compile to accept `HTTP/1.0` and mark the request as ancient, which makes uWebSockets close the connection after the response, as HTTP/1.0 expects.
8
+
9
+ ## 0.4.18
10
+
11
+ ### Patch Changes
12
+
13
+ - e0d0e17: Compile every unit with `-ffp-contract=off` (baronunread/sproutboat#235). On arm64, clang fused `a * b + c` into a single FMA that rounds once, where JavaScript rounds after the multiply and again after the add, so native sprouts could compute different doubles from V8 for the same code. A seeded LCG diverged at step 23.
14
+
3
15
  ## 0.4.17
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -29,7 +29,10 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
29
29
  " return -1;\n" +
30
30
  "}\n" +
31
31
  "#define PORF_CORO_STACK_SIZE (256u * 1024u)\n",
32
- "compiler/index.js": " ...darwinReleaseCompileArgs,\n uSocketsArchive,\n '-lm'\n",
32
+ "compiler/index.js":
33
+ "import { hashId } from './modules.js';\n" +
34
+ " const uwsDir = uwebsockets.ensureUWebSockets();\n" +
35
+ " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n ...darwinReleaseCompileArgs,\n uSocketsArchive,\n '-lm'\n",
33
36
  "compiler/precompile.js":
34
37
  "const fs = require('node:fs');\n" +
35
38
  "const path = require('node:path');\n" +
@@ -46,6 +49,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
46
49
  " len = i;\n" +
47
50
  " }\n\n arr.length = len;\n\n return new ${name}(arr);\n};\n",
48
51
  "compiler/uwebsockets.js":
52
+ 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n' +
49
53
  "static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;\n" +
50
54
  "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
51
55
  "static std::string_view lookup_status_line(i32 status) {\n" +
@@ -198,6 +202,13 @@ test("the default call (no url/expectedSha256 override) never touches the networ
198
202
  },
199
203
  });
200
204
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
205
+ // #236: the native-fetch build edits uWebSockets to accept HTTP/1.0.
206
+ const index = await readFile(join(dir, "compiler/index.js"), "utf8");
207
+ expect(index).toContain("import { sbPatchHttp10 } from './sb-http10.js';");
208
+ expect(index).toContain("const uwsDir = uwebsockets.ensureUWebSockets();\n sbPatchHttp10(uwsDir);");
209
+ expect(await readFile(join(dir, "compiler/sb-http10.js"), "utf8")).toContain("export const sbPatchHttp10");
210
+ // #235: every compiled unit keeps JavaScript's separate float roundings.
211
+ expect(await readFile(join(dir, "compiler/index.js"), "utf8")).toContain("'-ffp-contract=off',");
201
212
  // #168: the real vendored source takes the null guard on the `then` probe.
202
213
  expect(await readFile(join(dir, "compiler/builtins/promise.ts"), "utf8")).toContain(
203
214
  "if (Porffor.type(value) == Porffor.TYPES.object && value != null) {",
package/src/patch.test.ts CHANGED
@@ -44,7 +44,9 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
44
44
  // body limit (#56), the #156 status-line switch, #163's collect_headers, and
45
45
  // #176's read_value forward decl / is_forbidden_response_header /
46
46
  // write_response_value.
47
- const SHIM = `static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
47
+ const SHIM = `export const makeUWebSocketsShimSource = () => \`
48
+ #include "App.h"
49
+ static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
48
50
 
49
51
  int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);
50
52
 
package/src/patch.ts CHANGED
@@ -192,6 +192,69 @@ const CFLAGS_INJECT =
192
192
  " ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
193
193
  const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
194
194
 
195
+ // baronunread/sproutboat#235: clang's default -ffp-contract=on fuses `a * b + c`
196
+ // into one FMA on arm64, rounding once where JavaScript rounds twice. A seeded
197
+ // LCG (seed * 1103515245 + 12345, past 2^53) diverged from V8 at step 23.
198
+ // Every compiled unit keeps JS's separate roundings with contraction off.
199
+ const FP_CONTRACT_ANCHOR = " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n";
200
+ const FP_CONTRACT_INJECT = " '-ffp-contract=off', // sproutboat #235: JS rounds a * b + c twice\n";
201
+ const FP_CONTRACT_MARKER = "sproutboat #235";
202
+
203
+ // baronunread/sproutboat#236: uWebSockets answers any request line that isn't
204
+ // HTTP/1.1 with 505, so a standalone sprout behind nginx's default
205
+ // `proxy_http_version 1.0` failed every request. Its HttpRequest already has
206
+ // isAncient(), which closes the connection after the response; the parser just
207
+ // never sets it. uWebSockets is header-only and lives in Porffor's deps cache,
208
+ // not in the Porffor checkout, so a helper written next to compiler/index.js
209
+ // edits HttpParser.h right before each native-fetch compile (marker-guarded).
210
+ const HTTP10_HELPER = String.raw`// sproutboat #236: accept HTTP/1.0 request lines in uWebSockets.
211
+ import fs from 'node:fs';
212
+
213
+ const MARK = 'sb_http10_request';
214
+ const edits = [
215
+ [
216
+ 'struct HttpParser {',
217
+ '/* sproutboat #236: set by consumeRequestLine for an HTTP/1.0 request line. */\ninline thread_local bool sb_http10_request = false;\n\nstruct HttpParser {'
218
+ ],
219
+ [
220
+ '/* Scan until single SP, assume next is / (origin request) */\n char *start = data;',
221
+ '/* Scan until single SP, assume next is / (origin request) */\n sb_http10_request = false;\n char *start = data;'
222
+ ],
223
+ [
224
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }',
225
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }'
226
+ ],
227
+ [
228
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }',
229
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, 11) == 0) {\n sb_http10_request = true;\n return data + 11;\n }'
230
+ ],
231
+ [ 'req->ancientHttp = false;', 'req->ancientHttp = sb_http10_request;' ]
232
+ ];
233
+
234
+ export const sbPatchHttp10 = uwsDir => {
235
+ const file = uwsDir + '/src/HttpParser.h';
236
+ let src = fs.readFileSync(file, 'utf8');
237
+ if (src.includes(MARK)) return;
238
+ for (const [ from, to ] of edits) {
239
+ if (!src.includes(from)) throw new Error('sproutboat #236: uWebSockets HttpParser.h changed, anchor not found: ' + from.slice(0, 60));
240
+ src = src.replace(from, to);
241
+ }
242
+ fs.writeFileSync(file, src);
243
+ };
244
+ `;
245
+ const HTTP10_IMPORT_ANCHOR = "import { hashId } from './modules.js';\n";
246
+ const HTTP10_IMPORT_INJECT = "import { sbPatchHttp10 } from './sb-http10.js'; // sproutboat #236\n";
247
+ const HTTP10_IMPORT_MARKER = "./sb-http10.js";
248
+ const HTTP10_CALL_ANCHOR = " const uwsDir = uwebsockets.ensureUWebSockets();\n";
249
+ const HTTP10_CALL_INJECT = " sbPatchHttp10(uwsDir);\n";
250
+ const HTTP10_CALL_MARKER = "sbPatchHttp10(uwsDir)";
251
+ // The shim is content-cached, so a header edit alone would never recompile it.
252
+ // This line changes the shim once, which rebuilds it against the edited header.
253
+ const HTTP10_SHIM_ANCHOR = 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n';
254
+ const HTTP10_SHIM_INJECT =
255
+ 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n// sproutboat #236: built against an HttpParser.h that accepts HTTP/1.0\n';
256
+ const HTTP10_SHIM_MARKER = "sproutboat #236: built against";
257
+
195
258
  // baronunread/sproutboat#168: Porffor represents null as an object-typed value
196
259
  // with pointer 0, so __Porffor_promise_resolve's cheap `then` probe treated a
197
260
  // promise resolving to null as an object and looked `then` up at memory offset
@@ -767,6 +830,7 @@ const done = new Set<string>();
767
830
 
768
831
  /** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
769
832
  const UWS_EDITS = [
833
+ [HTTP10_SHIM_MARKER, HTTP10_SHIM_ANCHOR, HTTP10_SHIM_INJECT, "shim rebuild for HTTP/1.0 (#236)"],
770
834
  [BODY_MARKER, BODY_ANCHOR, BODY_INJECT, "request body limit"],
771
835
  [STATUS_SIG_MARKER, STATUS_SIG_ANCHOR, STATUS_SIG_INJECT, "status-line return type (#156)"],
772
836
  [STATUS_303_MARKER, STATUS_303_ANCHOR, STATUS_303_INJECT, "status-line 303 case (#156)"],
@@ -953,12 +1017,16 @@ export async function patchUwebsockets(root: string): Promise<void> {
953
1017
  }
954
1018
 
955
1019
  async function patchCompilerArgs(root: string): Promise<void> {
1020
+ await writeFile(resolve(root, "compiler/sb-http10.js"), HTTP10_HELPER);
956
1021
  const file = resolve(root, "compiler/index.js");
957
1022
  let src = await readFile(file, "utf8");
958
1023
  let changed = false;
959
1024
  for (const [marker, anchor, inject, what] of [
960
1025
  [LINK_MARKER, LINK_ANCHOR, LINK_INJECT, "extra link args"],
961
1026
  [CFLAGS_MARKER, CFLAGS_ANCHOR, CFLAGS_INJECT, "extra compiler flags"],
1027
+ [FP_CONTRACT_MARKER, FP_CONTRACT_ANCHOR, FP_CONTRACT_INJECT, "floating-point contraction"],
1028
+ [HTTP10_IMPORT_MARKER, HTTP10_IMPORT_ANCHOR, HTTP10_IMPORT_INJECT, "HTTP/1.0 helper import (#236)"],
1029
+ [HTTP10_CALL_MARKER, HTTP10_CALL_ANCHOR, HTTP10_CALL_INJECT, "HTTP/1.0 parser edit (#236)"],
962
1030
  ] as const) {
963
1031
  if (src.includes(marker)) continue;
964
1032
  const at = src.indexOf(anchor);
@@ -971,7 +1039,7 @@ async function patchCompilerArgs(root: string): Promise<void> {
971
1039
  // Add flags to compileOnlyArgs so every generated C unit sees them and
972
1040
  // Porffor's module-build cache includes them in its compiler command.
973
1041
  src =
974
- marker === CFLAGS_MARKER
1042
+ marker !== LINK_MARKER
975
1043
  ? src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length)
976
1044
  : src.slice(0, at) + inject + src.slice(at);
977
1045
  changed = true;