diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d0cf8f..1c89373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(sxn VERSION 0.0.1 LANGUAGES C) +project(sxn VERSION 0.0.2 LANGUAGES C) # Every other dependency here (curl, zlib, libuv, libffi) resolves to a # dylib under /usr/lib on macOS - part of the OS, present on every Mac. # OpenSSL doesn't ship there anymore, so find_package(OpenSSL) resolves to @@ -9,8 +9,32 @@ project(sxn VERSION 0.0.1 LANGUAGES C) if(APPLE) set(OPENSSL_USE_STATIC_LIBS TRUE) endif() -find_package(OpenSSL REQUIRED) -find_package(CURL REQUIRED) +if(DEFINED SXN_CROSS_DEPS_ROOT) + # Cross builds cannot use the host's FindOpenSSL/CURL/ZLIB results. The + # release builder supplies a target sysroot containing import libraries. + set(_cross_root "${SXN_CROSS_DEPS_ROOT}") + add_library(OpenSSL::Crypto UNKNOWN IMPORTED) + set_target_properties(OpenSSL::Crypto PROPERTIES + IMPORTED_LOCATION "${_cross_root}/lib/libcrypto.dll.a" + INTERFACE_INCLUDE_DIRECTORIES "${_cross_root}/include") + add_library(OpenSSL::SSL UNKNOWN IMPORTED) + set_target_properties(OpenSSL::SSL PROPERTIES + IMPORTED_LOCATION "${_cross_root}/lib/libssl.dll.a" + INTERFACE_INCLUDE_DIRECTORIES "${_cross_root}/include" + INTERFACE_LINK_LIBRARIES OpenSSL::Crypto) + add_library(CURL::libcurl UNKNOWN IMPORTED) + set_target_properties(CURL::libcurl PROPERTIES + IMPORTED_LOCATION "${_cross_root}/lib/libcurl.dll.a" + INTERFACE_INCLUDE_DIRECTORIES "${_cross_root}/include" + INTERFACE_LINK_LIBRARIES "ws2_32;crypt32;bcrypt;advapi32") + add_library(ZLIB::ZLIB UNKNOWN IMPORTED) + set_target_properties(ZLIB::ZLIB PROPERTIES + IMPORTED_LOCATION "${_cross_root}/lib/libz.dll.a" + INTERFACE_INCLUDE_DIRECTORIES "${_cross_root}/include") +else() + find_package(OpenSSL REQUIRED) + find_package(CURL REQUIRED) +endif() # libuv ships a CMake config package from Homebrew and vcpkg, but Ubuntu's # libuv1-dev provides only pkg-config, so fall back to that rather than # requiring the config package everywhere. @@ -71,7 +95,7 @@ endif() # writing to a directory the other one hasn't created yet. file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/generated) -# WinterCG globals (TextEncoder, URL, Headers, fetch's JS glue, ...) are +# WinterTC globals (TextEncoder, URL, Headers, fetch's JS glue, ...) are # authored as plain JS in src/bootstrap.js and compiled to bytecode at build # time, so startup reads a prepared function instead of parsing 140KB of # JavaScript on every launch -- worth about 3ms of a 10ms cold start. @@ -94,7 +118,7 @@ add_custom_target(sxn_bootstrap_header DEPENDS ${SXN_BOOTSTRAP_HEADER}) # node:buffer/path/events/process are authored and compiled the same way # (src/node_compat.js), kept as a separate file/header from bootstrap.js -# since it's a distinct WinterCG-vs-Node-compat concern. +# since it's a distinct WinterTC-vs-Node-compat concern. set(SXN_NODE_COMPAT_JS ${CMAKE_CURRENT_SOURCE_DIR}/src/node_compat.js) set(SXN_NODE_COMPAT_HEADER ${CMAKE_CURRENT_BINARY_DIR}/generated/sxn_node_compat.h) add_custom_command( @@ -186,6 +210,42 @@ if(BUILD_TESTING AND SXN_BUILD_TESTS) add_test(NAME sxn-sx-module COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/import-main.sx) add_test(NAME sxn-reject-enum COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/reject-enum.sx) set_tests_properties(sxn-reject-enum PROPERTIES WILL_FAIL TRUE) + # `&mut` requires a mutable owner. The legal direction is covered by + # sxn-example (velocity.sx) and sxn-example-hello, both of which borrow a + # `let mut`; this is the rejection side. + add_test(NAME sxn-reject-mut-borrow COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/reject-mut-borrow.sx) + set_tests_properties(sxn-reject-mut-borrow PROPERTIES WILL_FAIL TRUE) + # `safe` specializes on the declared type, so i32 wraps and every other + # annotation keeps exact JS arithmetic. Both right-hand-side shapes are + # covered: a constant and a local compile through different peephole + # branches and used to disagree. + add_test(NAME sxn-typed-overflow COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/typed_overflow.sx) + set_tests_properties(sxn-typed-overflow PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # A call to a small function with a declared scalar signature is spliced + # into its caller. This asserts the splice changes nothing observable: same + # values, same coercions, same exceptions, and no splice at all when the + # binding is reassigned, captured, or the shape does not qualify. Every + # expected value here is Node's, checked with the annotations stripped. + add_test(NAME sxn-typed-inline COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/typed_inline.sx) + set_tests_properties(sxn-typed-inline PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # Reference cycles are invisible to refcounting and reclaimable only by the + # cycle collector, which runs only on allocation. Sxn.gc() is the explicit + # ask; this asserts a burst of 100k cyclic closures comes back. + add_test(NAME sxn-leak-cycle-stress COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/leak_cycle_stress.sx) + set_tests_properties(sxn-leak-cycle-stress PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # And the same reclamation with nobody asking: a daemon that takes a burst + # and then waits must give the memory back on its own. Run both ways, since + # the whole point of --no-idle-gc is that it does not. + add_test(NAME sxn-leak-idle-sweep COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/leak_idle_sweep.mjs) + set_tests_properties(sxn-leak-idle-sweep PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + add_test(NAME sxn-leak-idle-sweep-off + COMMAND sxn --no-idle-gc ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/leak_idle_sweep.mjs --expect-no-sweep) + set_tests_properties(sxn-leak-idle-sweep-off PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # Listeners added and never removed are how a server grows one closure at a + # time. Every expectation here is Node's: the fixture passes unchanged under + # `node tests/fixtures/events_maxlisteners.mjs`. + add_test(NAME sxn-events-maxlisteners COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/events_maxlisteners.mjs) + set_tests_properties(sxn-events-maxlisteners PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") # Regression coverage: SX's contextual `interface` grammar hook must not # leak into ordinary .cjs execution (see update_token_ident in quickjs.c). add_test(NAME sxn-interface-ident COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/interface-ident.cjs) @@ -325,6 +385,9 @@ if(BUILD_TESTING AND SXN_BUILD_TESTS) # node:stream: flowing and paused modes, pipe, transform, pipeline, the # Buffer conversion outside objectMode, and the Web Streams bridges. add_test(NAME sxn-node-stream COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_stream.mjs) + # This one prints its results in one go at the end, so silence is failure: + # require the last line rather than merely the absence of FAIL. + set_tests_properties(sxn-node-stream PROPERTIES PASS_REGULAR_EXPRESSION "bytes join into one read") set_tests_properties(sxn-node-stream PROPERTIES TIMEOUT 40) # node:http: the (req, res) server, headers, chunked writes, request bodies # and a response finished after the handler returns. @@ -334,6 +397,55 @@ if(BUILD_TESTING AND SXN_BUILD_TESTS) add_test(NAME sxn-node-http COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_http.mjs) add_test(NAME sxn-node-zlib COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_zlib.mjs) add_test(NAME sxn-node-crypto-net COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_crypto_net.mjs) + # node:os and the parts of node:fs a server needs, against the real machine + # rather than against stubbed answers. + add_test(NAME sxn-node-os-fs COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_os_fs.mjs) + set_tests_properties(sxn-node-os-fs PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # node:querystring, now native C, against what Node itself printed. + add_test(NAME sxn-node-querystring COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_querystring.mjs) + set_tests_properties(sxn-node-querystring PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # node:path, posix and win32, against what Node prints for the same corpus. + add_test(NAME sxn-node-path COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_path.mjs) + set_tests_properties(sxn-node-path PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # net.isIP, against Node's own answers. + add_test(NAME sxn-node-isip COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_isip.mjs) + set_tests_properties(sxn-node-isip PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # HMAC and timingSafeEqual, against Node's own digests. + add_test(NAME sxn-node-hmac COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_hmac.mjs) + set_tests_properties(sxn-node-hmac PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # Buffer's numeric accessors and compare, against Node's own answers. + add_test(NAME sxn-node-buffer-numbers COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_buffer_numbers.mjs) + set_tests_properties(sxn-node-buffer-numbers PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # require() of every builtin, both spellings, now a native table lookup. + add_test(NAME sxn-node-builtin-require COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_builtin_require.mjs) + set_tests_properties(sxn-node-builtin-require PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # latin1/ascii/utf16le in both directions, now native, against Node. + add_test(NAME sxn-node-buffer-units COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_buffer_units.mjs) + set_tests_properties(sxn-node-buffer-units PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # StringDecoder's utf-8 path, native, against every way of splitting a + # multi-byte character across chunks. + add_test(NAME sxn-node-string-decoder COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_string_decoder.mjs) + set_tests_properties(sxn-node-string-decoder PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # zlib keeps its stream between calls and resets it; the bytes must not + # change, across window bits, levels and sizes. + add_test(NAME sxn-node-zlib-reuse COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_zlib_reuse.mjs) + set_tests_properties(sxn-node-zlib-reuse PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # util.inspect, native: every kind of value, the depth limit and a cycle. + add_test(NAME sxn-node-inspect COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_inspect.mjs) + set_tests_properties(sxn-node-inspect PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # process.nextTick carries its arguments natively. + add_test(NAME sxn-node-next-tick COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_next_tick.mjs) + set_tests_properties(sxn-node-next-tick PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # process.cwd() is cached; process.chdir() is what clears it. + add_test(NAME sxn-node-process-cwd COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_process_cwd.mjs) + set_tests_properties(sxn-node-process-cwd PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # node:assert's structural comparison, now native C, against Node's answers + # for the same 130 pairs. + add_test(NAME sxn-node-assert COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_assert.mjs) + set_tests_properties(sxn-node-assert PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # util.format, against Node's own output. + add_test(NAME sxn-node-format COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_format.mjs) + set_tests_properties(sxn-node-format PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") # Every Buffer encoding in both directions, including Node's lenient hex and # base64 readers. Expectations are Node's own output, so a divergence fails. add_test(NAME sxn-buffer-encodings COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/buffer_encodings.mjs) @@ -388,6 +500,43 @@ if(BUILD_TESTING AND SXN_BUILD_TESTS) add_test(NAME sxn-serve-headers COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/serve_headers.mjs) set_tests_properties(sxn-serve-headers PROPERTIES TIMEOUT 30 FAIL_REGULAR_EXPRESSION "FAIL") add_test(NAME sxn-serve-fetch-shape COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/serve_fetch_shape.mjs) + # A request larger than one read: the server has to accumulate reads until + # the head and the announced body are both in hand. + add_test(NAME sxn-serve-large-body COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/serve_large_body.mjs) + set_tests_properties(sxn-serve-large-body PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # HTTP keep-alive: a connection is reused for the next request, and stop() + # closes the ones still open. + add_test(NAME sxn-serve-keepalive COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/serve_keepalive.mjs) + set_tests_properties(sxn-serve-keepalive PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # Where a server binds: the hostname option, and reusePort for running one + # process per core. + add_test(NAME sxn-serve-bind COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/serve_bind.mjs) + set_tests_properties(sxn-serve-bind PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # A served request's headers are built on first read; they still have to + # behave as if they had been built up front. + add_test(NAME sxn-serve-lazy-headers COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/serve_lazy_headers.mjs) + set_tests_properties(sxn-serve-lazy-headers PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # The request body reaches a handler as the bytes the server read, without + # a string in between -- including over a reused connection. + add_test(NAME sxn-serve-body-bytes COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/serve_body_bytes.mjs) + set_tests_properties(sxn-serve-body-bytes PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # JSON.parse/JSON.stringify at the edges of their fast paths: escapes, + # surrogates, non-ASCII, control characters and every number form. The + # expectations are Node's own output, so a divergence fails. + add_test(NAME sxn-json-edges COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/json_edges.mjs) + set_tests_properties(sxn-json-edges PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") + # Every name the Minimum Common API asks for, and what the new ones do: + # URLPattern's matching, the compression streams, BYOB reads. + add_test(NAME sxn-wintertc-surface COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/wintertc_surface.mjs) + set_tests_properties(sxn-wintertc-surface PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # The builtins beyond the original 24, exercised rather than probed: a real + # child process, a real DNS answer, a real UDP round trip. + add_test(NAME sxn-node-new-builtins COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/node_new_builtins.mjs) + set_tests_properties(sxn-node-new-builtins PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL") + # 4000 seeded-random documents through parse and stringify, hashed. The + # expected hash is Node's, so one byte of divergence anywhere fails. + add_test(NAME sxn-json-fuzz COMMAND sxn ${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/json_fuzz.mjs) + set_tests_properties(sxn-json-fuzz PROPERTIES TIMEOUT 120 FAIL_REGULAR_EXPRESSION "FAIL") set_tests_properties(sxn-serve-fetch-shape PROPERTIES TIMEOUT 30 FAIL_REGULAR_EXPRESSION "FAIL") set_tests_properties(sxn-performance-now PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") set_tests_properties(sxn-encode-into PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2de03d..e36caf0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ site, built from [`docs/index.html`](docs/index.html) and published by **Genuinely open to change**, including disagreement with the current approach: the shape and scope of `safe`/`unsafe`, which TypeScript forms get real support next, ownership and borrow-checking rules and their error -messages, `node:`/WinterCG coverage priorities, naming, ergonomics — and the +messages, `node:`/WinterTC coverage priorities, naming, ergonomics — and the underlying ideas themselves. If you think the ownership model solves the wrong problem, open an issue and make the case. diff --git a/README.md b/README.md index 3d15d7b..27e5d82 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,13 @@ irm https://sxfescript.github.io/latest/install.ps1 | iex ``` Both install to `~/.sxn/bin` (`%USERPROFILE%\.sxn\bin` on Windows) and add it -to your PATH. Swap `latest` for a version tag (`v0.0.1`) in either URL to pin +to your PATH. Swap `latest` for a version tag (`v0.0.2`) in either URL to pin a specific release instead of always getting the newest one. +The 0.0.2 pre-release includes binaries for macOS arm64/x64, Linux arm64/x64, +and Windows arm64/x64. The release builds are documented in [`llm.txt`](llm.txt) +and the reproducible packaging entry point is [`scripts/release.sh`](scripts/release.sh). + ## Build Needs OpenSSL, libcurl, libuv, zlib, and libffi on the system (`brew install @@ -99,19 +103,30 @@ opcode lowering, full control-flow ownership pass, native npm registry backend, and semantic LSP features are tracked in `spec/IMPLEMENTATION.md` and are not yet represented as complete production implementations. +Two things the annotations do today rather than being stripped. `&mut` requires +a `let mut` owner, so borrowing an immutable binding is a compile error naming +SX2003 -- the one ownership rule enforced ahead of that control-flow pass. And +the declared type reaches codegen: `safe let mut n: i32` wraps at the 32-bit +boundary where every other annotation keeps exact JavaScript arithmetic, and a +small function with a fully declared scalar signature is inlined into its +caller, worth 3.1x on a two-argument add. `spec/PERFORMANCE.md` has the +measurements. + ## What the runtime does Two documents cover what actually runs, and split the same way the codebase does: -- **`spec/RUNTIME.md`** -- the WinterCG web APIs and the `Sxn` host namespace: - `fetch`, `Sxn.serve` (HTTP, SSE, WebSocket upgrade), Web Streams, Web - Crypto, `structuredClone`, and `Sxn.ffi` for calling a C function directly. +- **`spec/RUNTIME.md`** -- the WinterTC web APIs and the `Sxn` host namespace: + `fetch`, `Sxn.serve` (HTTP, SSE, WebSocket upgrade), Web Streams, + `URLPattern`, Web Crypto, `structuredClone`, `Sxn.memoryUsage()` and + `Sxn.gc()`, and `Sxn.ffi` for calling a C function directly. Every name in + the Minimum Common API is there except WebAssembly's. This is the half that travels when the engine is embedded elsewhere, and the only half a mobile build needs. - **`spec/NODE.md`** -- what makes `sxn` usable as a Node alternative: - CommonJS, `node:` builtins (24 of ~37), and `.node` native-addon loading - through a from-scratch Node-API implementation. This half exists to + CommonJS, a superset of the `node:` builtins, and `.node` native-addon + loading over 120 Node-API entry points. This half exists to emulate Node and nothing else, so a build with no Node surface drops it and loses nothing on the runtime side. @@ -131,7 +146,7 @@ negligible on a one-liner. ## Benchmarks: sxn vs Node vs Bun -`benchmarks/wintercg/run.sh` runs matched WinterCG-style workloads against +`benchmarks/wintertc/run.sh` runs matched WinterTC-style workloads against `sxn`, Node and Bun side by side. No category is hidden -- the others win the ones you'd expect them to. Each runtime runs the same workload with the same iteration counts, written in that runtime's @@ -141,14 +156,14 @@ three. Bun is optional -- its rows are skipped with a note if it isn't installed. ```sh -sh benchmarks/wintercg/run.sh +sh benchmarks/wintertc/run.sh ``` For performance measurements, use the optimized binary explicitly; the script accepts any SXN path. For example: ```sh -RUNS=1000 SXN=build/release/sxn sh benchmarks/wintercg/run.sh +RUNS=1000 SXN=build/release/sxn sh benchmarks/wintertc/run.sh ``` Keep Debug for leak and correctness checks; Release is the appropriate binary @@ -166,14 +181,15 @@ working laptop under load, and the Linux box is slower per core but idle. | Memory | 16 GB | 13 GB | | OS | macOS 26.6.2 (arm64) | Ubuntu 23.10, kernel 6.5.0-44 (x86_64) | | Compiler | Apple clang | gcc 13.2 | -| Node | v25.2.1 | v18.13.0 | -| Bun | 1.2.17 | 1.2.17 | +| Node | v25.2.1 | v23.11.1 | +| Bun | 1.2.17 | 1.2.21 | | Load while measuring | 2-5 | 0.4-1.2 | -Read each machine's table against itself, never across the two. The Linux -Node is four major versions behind, and `performance.now` costs far more per -call on that kernel, which is why its pause totals read in seconds for all -three runtimes. Same tree, same tests, same 66 fixtures passing on both. +Read each machine's table against itself, never across the two. Both now run +the same major Node; the Linux box's Bun is a few patches ahead. What still +differs is the kernel: `performance.now` costs far more per call there, which +is why its pause totals read in seconds for all three runtimes. Same tree, +same tests, same 95 fixtures passing on both. How each row is measured: throughput rows are the harness's own 1,000-run medians. The two startup rows are 20 interleaved launches per runtime, quoted @@ -187,40 +203,42 @@ runtime's startup cost. | Category | sxn | Node | Bun | Winner | |---|---|---|---|---| -| Real-world end-to-end task | **10.4 ms** | 76.3 ms | 15.5 ms | sxn | -| Cold start | **8.4 ms** | 41.6 ms | 9.2 ms | sxn | -| Sustained throughput: Buffer ops | **19.2 ms** | 23.8 ms | 27.6 ms | sxn | -| Sustained throughput: TextEncoder | **4.7 ms** | 38.9 ms | 6.3 ms | sxn | -| Sustained throughput: EventEmitter | 6.6 ms | **5.1 ms** | 9.3 ms | Node | -| Pause consistency: total time | **147.8 ms** | 242.5 ms | 283.1 ms | sxn | -| Pause consistency: worst single pause | **0.04 ms** | 0.36 ms | 2.59 ms | sxn | -| Parse 32k-line generated file | **20.9 ms** | 51.0 ms | 24.3 ms | sxn | - -Seven of eight, holding steady since the last pass -- these numbers include -the class-constructor and thread-safe-function work, and neither moved a -row. EventEmitter is the one Node keeps, and its 1.1x here is a JIT inlining -a call to nothing: an ablation that skips the fused call's guards entirely -still only reaches 4.7 ms, because roughly a third of the row is this -interpreter's own loop dispatch. +| Real-world end-to-end task | **8.4 ms** | 76.6 ms | 15.6 ms | sxn | +| Cold start | **7.5 ms** | 42.5 ms | 9.4 ms | sxn | +| Sustained throughput: Buffer ops | **19.4 ms** | 24.5 ms | 27.1 ms | sxn | +| Sustained throughput: TextEncoder | **4.7 ms** | 39.8 ms | 6.2 ms | sxn | +| Sustained throughput: EventEmitter | 6.7 ms | **5.4 ms** | 9.2 ms | Node | +| Sustained throughput: JSON round trip | 48.0 ms | 29.3 ms | **24.8 ms** | Bun | +| Pause consistency: total time | **146.6 ms** | 241.7 ms | 277.0 ms | sxn | +| Pause consistency: worst single pause | **0.01 ms** | 0.28 ms | 3.13 ms | sxn | +| Parse 32k-line generated file | **20.1 ms** | 49.9 ms | 25.6 ms | sxn | + +Seven of nine. The two that are not sxn's are the two worth reading: a JIT +inlines an EventEmitter call to nothing, and an ablation that skips this +interpreter's fused-call guards entirely still only reaches 4.7 ms, because +roughly a third of that row is loop dispatch. JSON is a megabyte parsed and +written back forty times, and the gap there is the same story with more code +in it -- `JSON.parse` is C in all three, but what surrounds it is not. ### Linux PC (Ryzen 7 5700G) -| Category | sxn | Node 18 | Bun | Winner | +| Category | sxn | Node | Bun | Winner | |---|---|---|---|---| -| Real-world end-to-end task | **6.9 ms** | 224.0 ms | 23.2 ms | sxn | -| Cold start | **7.6 ms** | 117.1 ms | 15.1 ms | sxn | -| Sustained throughput: Buffer ops | **37.4 ms** | 75.6 ms | 83.0 ms | sxn | -| Sustained throughput: TextEncoder | **8.6 ms** | 89.2 ms | 16.2 ms | sxn | -| Sustained throughput: EventEmitter | 14.8 ms | **13.0 ms** | 23.2 ms | Node | -| Pause consistency: total time | **2836.0 ms** | 3463.2 ms | 3219.4 ms | sxn | -| Pause consistency: worst single pause | **0.30 ms** | 4.96 ms | 5.67 ms | sxn | -| Parse 32k-line generated file | **34.8 ms** | 144.3 ms | 54.1 ms | sxn | - -Seven of eight, and the numbers are far steadier than anything the laptop can -produce. Both machines agree on which row is which: sxn takes everything -except EventEmitter, and that one is Node's on both, which is the point -- -it is the one row where the gap is architectural rather than incidental. The -Linux gap is the narrower of the two, 1.1x against the Mac's 1.3x. +| Real-world end-to-end task | **7.5 ms** | 56.7 ms | 22.4 ms | sxn | +| Cold start | **7.4 ms** | 23.1 ms | 13.3 ms | sxn | +| Sustained throughput: Buffer ops | **38.0 ms** | 39.2 ms | 83.2 ms | sxn | +| Sustained throughput: TextEncoder | **9.1 ms** | 80.6 ms | 18.0 ms | sxn | +| Sustained throughput: EventEmitter | 15.9 ms | **10.1 ms** | 25.2 ms | Node | +| Sustained throughput: JSON round trip | 82.9 ms | 113.5 ms | **60.3 ms** | Bun | +| Pause consistency: total time | **2855.9 ms** | 3295.3 ms | 3252.4 ms | sxn | +| Pause consistency: worst single pause | **0.25 ms** | 1.72 ms | 6.48 ms | sxn | +| Parse 32k-line generated file | **36.6 ms** | 51.5 ms | 54.2 ms | sxn | + +Seven of nine again, and the same two are not sxn's, which is the useful +part: two machines, two chips, two operating systems, and the shape of the +result does not move. Buffer is the one row where Node is close here rather +than behind, and JSON is closer than it is on the Mac -- against this Node, +sxn takes JSON while Bun keeps it. The full write-up -- pause-row detail, the no-JIT tradeoff, every optimization behind these numbers in the order it landed, and what's still diff --git a/benchmarks/engine/call_inline_probe.sx b/benchmarks/engine/call_inline_probe.sx new file mode 100644 index 0000000..697b671 --- /dev/null +++ b/benchmarks/engine/call_inline_probe.sx @@ -0,0 +1,52 @@ +// What the typed-call inliner is worth, and against what ceiling. +// +// A call to a function with a fully declared scalar signature is spliced into +// its caller by sx_inline_typed_calls (quickjs.c). "hand-inlined" is the same +// arithmetic written out by hand and is therefore the exact upper bound; the +// untyped row is the same program with the annotations removed, which never +// qualifies and so measures the call this pass removes. +// +// Report is per loop iteration, minimum of 9 runs. Subtract the empty loop to +// read the cost of the work itself. +// +// sxn benchmarks/engine/call_inline_probe.sx + +const N = 5000000; + +function bench(name: string, fn): void { + let best = Infinity; + for (let r = 0; r < 9; r++) { + const t = performance.now(); + fn(); + const d = performance.now() - t; + if (d < best) best = d; + } + console.log(name.padEnd(24), (best * 1e6 / N).toFixed(2), "ns/op"); +} + +function emptyLoop(): number { let s = 0; for (let i = 0; i < N; i++) ; return s; } + +function typedCall(): number { + function add2(a: i32, b: i32): i32 { return a + b; } + let s = 0; + for (let i = 0; i < N; i++) s = add2(i, 1); + return s; +} + +function untypedCall(): number { + function add2(a, b) { return a + b; } + let s = 0; + for (let i = 0; i < N; i++) s = add2(i, 1); + return s; +} + +function handInlined(): number { + let s = 0; + for (let i = 0; i < N; i++) s = i + 1; + return s; +} + +bench("empty loop", emptyLoop); +bench("untyped call", untypedCall); +bench("typed call", typedCall); +bench("hand-inlined", handInlined); diff --git a/benchmarks/engine/gc_idle_probe.sx b/benchmarks/engine/gc_idle_probe.sx new file mode 100644 index 0000000..a577777 --- /dev/null +++ b/benchmarks/engine/gc_idle_probe.sx @@ -0,0 +1,59 @@ +// What a burst of cyclic garbage costs a long-running process, and what a +// sweep gets back. +// +// sxn benchmarks/engine/gc_idle_probe.sx +// +// Two numbers per row, because they answer different questions and move +// independently: +// +// tracked the engine allocator's own accounting, which is what the GC +// threshold is compared against and what a leak test can assert on. +// rss what the operating system says the process is holding. The +// system allocator is free to keep freed pages rather than return +// them, so rss can stay high after a sweep that reclaimed +// everything. Report it, never assert on it. +// +// TICKS is 200,000 rather than the millions a soak test would use. The effect +// is a step function, not a slow drift: the burst either survives the drop or +// it does not, and 200k already puts ~200 MB on the wrong side of the 32 MB +// idle-sweep floor. Raise it to watch a bigger heap; it will not change the +// shape of the answer. +const TICKS = 200000; + +function row(label: string): void { + const m = Sxn.memoryUsage(); + const mb = (n) => (n / (1024 * 1024)).toFixed(1).padStart(8); + console.log(label.padEnd(28), "tracked", mb(m.mallocSize), "MB rss", + mb(m.rss), "MB collections", m.gcCount); +} + +// One simulated request: state that points at itself and a handler the state +// holds. Refcounting can never free either, whatever the request does next. +function tick(i: i32) { + const state = { id: i, body: new Array(24).fill(i) }; + state.self = state; + const respond = () => state.body.length; + state.respond = respond; + return respond; +} + +// The batch is held while it is built. That is what a burst looks like from +// the collector's side, and it is what raises the allocation threshold to 1.5x +// the peak -- the reason the garbage then survives being dropped. +function burst(n: i32) { + const batch = []; + for (let i = 0; i < n; i++) batch.push(tick(i)); + return batch.length; +} + +row("before the burst"); +burst(TICKS); +row("after it is dropped"); + +// Everything above this line is unreachable. Without a sweep it stays, and +// the next burst has to be half again as large before the allocator notices. +const reclaimed = Sxn.memoryUsage().mallocSize - Sxn.gc(); +row("after a sweep"); +console.log("\nreclaimed", (reclaimed / (1024 * 1024)).toFixed(1), "MB of unreachable cycles"); +console.log("A server left idle reclaims this on its own; see sxn --help for"); +console.log("--no-idle-gc and --idle-gc-floor=."); diff --git a/benchmarks/wintercg/coldstart.bun.js b/benchmarks/wintertc/coldstart.bun.js similarity index 100% rename from benchmarks/wintercg/coldstart.bun.js rename to benchmarks/wintertc/coldstart.bun.js diff --git a/benchmarks/wintercg/coldstart.js b/benchmarks/wintertc/coldstart.js similarity index 100% rename from benchmarks/wintercg/coldstart.js rename to benchmarks/wintertc/coldstart.js diff --git a/benchmarks/wintercg/coldstart.sx b/benchmarks/wintertc/coldstart.sx similarity index 100% rename from benchmarks/wintercg/coldstart.sx rename to benchmarks/wintertc/coldstart.sx diff --git a/benchmarks/wintercg/pause.bun.js b/benchmarks/wintertc/pause.bun.js similarity index 100% rename from benchmarks/wintercg/pause.bun.js rename to benchmarks/wintertc/pause.bun.js diff --git a/benchmarks/wintercg/pause.js b/benchmarks/wintertc/pause.js similarity index 100% rename from benchmarks/wintercg/pause.js rename to benchmarks/wintertc/pause.js diff --git a/benchmarks/wintercg/pause.sx b/benchmarks/wintertc/pause.sx similarity index 100% rename from benchmarks/wintercg/pause.sx rename to benchmarks/wintertc/pause.sx diff --git a/benchmarks/wintercg/realworld.bun.js b/benchmarks/wintertc/realworld.bun.js similarity index 100% rename from benchmarks/wintercg/realworld.bun.js rename to benchmarks/wintertc/realworld.bun.js diff --git a/benchmarks/wintercg/realworld.js b/benchmarks/wintertc/realworld.js similarity index 100% rename from benchmarks/wintercg/realworld.js rename to benchmarks/wintertc/realworld.js diff --git a/benchmarks/wintercg/realworld.sx b/benchmarks/wintertc/realworld.sx similarity index 92% rename from benchmarks/wintercg/realworld.sx rename to benchmarks/wintertc/realworld.sx index 247ea2a..381544e 100644 --- a/benchmarks/wintercg/realworld.sx +++ b/benchmarks/wintertc/realworld.sx @@ -1,4 +1,4 @@ -/* One realistic end-to-end WinterCG task: fetch, parse, dispatch events, +/* One realistic end-to-end WinterTC task: fetch, parse, dispatch events, encode, path/URL work. Wall-clock including process startup -- how a CLI tool, serverless function, or CI script is actually invoked. */ import { Buffer } from 'node:buffer'; diff --git a/benchmarks/wintercg/run.sh b/benchmarks/wintertc/run.sh similarity index 95% rename from benchmarks/wintercg/run.sh rename to benchmarks/wintertc/run.sh index b6aea1e..ba2cae4 100755 --- a/benchmarks/wintercg/run.sh +++ b/benchmarks/wintertc/run.sh @@ -2,14 +2,14 @@ # Side-by-side sxn vs node vs bun. No category is hidden. Each runtime runs the # same workload with the same iteration counts, written in that runtime's # idiomatic form (Bun.serve/Bun.env for bun, Sxn.serve for sxn); Buffer, -# TextEncoder and EventEmitter are the APIs under test and are the same in all -# three. +# TextEncoder, EventEmitter and JSON are the APIs under test and are the same +# in all three. # # Every runtime is overridable, because a non-interactive shell does not have # the PATH a login shell does and a runtime installed under ~/.local or ~/.bun # would otherwise be silently skipped: # -# SXN=build/release/sxn NODE=node BUN=~/.bun/bin/bun sh benchmarks/wintercg/run.sh +# SXN=build/release/sxn NODE=node BUN=~/.bun/bin/bun sh benchmarks/wintertc/run.sh # # bun stays optional and its rows are skipped with a note when it is absent. set -e @@ -74,7 +74,7 @@ measure_throughput() { i=0 while [ "$i" -lt "$RUNS" ]; do "$@" "$script" >>"$tmp"; i=$((i + 1)); done printf -- "-- %s (median) --\n" "$label" - for metric in buffer textencoder events; do + for metric in buffer textencoder events json; do value="$(awk -v metric="$metric" '$1 == metric ":" { print $2 }' "$tmp" | sort -n | median)" printf "%s: %s ms\n" "$metric" "$value" done diff --git a/benchmarks/wintercg/server.bun.js b/benchmarks/wintertc/server.bun.js similarity index 100% rename from benchmarks/wintercg/server.bun.js rename to benchmarks/wintertc/server.bun.js diff --git a/benchmarks/wintercg/server.sx b/benchmarks/wintertc/server.sx similarity index 100% rename from benchmarks/wintercg/server.sx rename to benchmarks/wintertc/server.sx diff --git a/benchmarks/wintercg/startup20.py b/benchmarks/wintertc/startup20.py similarity index 95% rename from benchmarks/wintercg/startup20.py rename to benchmarks/wintertc/startup20.py index 66746a7..d1498d5 100755 --- a/benchmarks/wintercg/startup20.py +++ b/benchmarks/wintertc/startup20.py @@ -1,5 +1,5 @@ import subprocess, time, sys, os, signal -DIR="benchmarks/wintercg"; SXN="build/release/sxn" +DIR="benchmarks/wintertc"; SXN="build/release/sxn" def run20(label, cmd, n=20): ts=[] for _ in range(n): diff --git a/benchmarks/wintercg/throughput.bun.js b/benchmarks/wintertc/throughput.bun.js similarity index 67% rename from benchmarks/wintercg/throughput.bun.js rename to benchmarks/wintertc/throughput.bun.js index 8c96595..04d774c 100644 --- a/benchmarks/wintercg/throughput.bun.js +++ b/benchmarks/wintertc/throughput.bun.js @@ -20,3 +20,17 @@ ee.on("x", (v) => { count += v; }); t0 = performance.now(); for (let i = 0; i < N; i++) ee.emit("x", i); console.log("events:", (performance.now() - t0).toFixed(1), "ms"); + +// JSON: a 1MB API payload, parsed and written back out. This is the shape of +// a request body in any JSON API, and both directions are under test. +const doc = JSON.stringify(Array.from({ length: 2000 }, (_, i) => ({ + id: i, + name: "record " + i, + tag: "t" + (i % 7), + bio: "the quick brown fox jumps over the lazy dog ".repeat(6), + ok: i % 2 === 0, + score: i * 3, +}))); +t0 = performance.now(); total = 0; +for (let i = 0; i < 40; i++) total += JSON.stringify(JSON.parse(doc)).length; +console.log("json:", (performance.now() - t0).toFixed(1), "ms"); diff --git a/benchmarks/wintercg/throughput.js b/benchmarks/wintertc/throughput.js similarity index 57% rename from benchmarks/wintercg/throughput.js rename to benchmarks/wintertc/throughput.js index b55e841..0b33947 100644 --- a/benchmarks/wintercg/throughput.js +++ b/benchmarks/wintertc/throughput.js @@ -14,3 +14,17 @@ ee.on("x", (v) => { count += v; }); t0 = performance.now(); for (let i = 0; i < N; i++) ee.emit("x", i); console.log("events:", (performance.now() - t0).toFixed(1), "ms"); + +// JSON: a 1MB API payload, parsed and written back out. This is the shape of +// a request body in any JSON API, and both directions are under test. +const doc = JSON.stringify(Array.from({ length: 2000 }, (_, i) => ({ + id: i, + name: "record " + i, + tag: "t" + (i % 7), + bio: "the quick brown fox jumps over the lazy dog ".repeat(6), + ok: i % 2 === 0, + score: i * 3, +}))); +t0 = performance.now(); total = 0; +for (let i = 0; i < 40; i++) total += JSON.stringify(JSON.parse(doc)).length; +console.log("json:", (performance.now() - t0).toFixed(1), "ms"); diff --git a/benchmarks/wintercg/throughput.sx b/benchmarks/wintertc/throughput.sx similarity index 58% rename from benchmarks/wintercg/throughput.sx rename to benchmarks/wintertc/throughput.sx index c419706..04c4804 100644 --- a/benchmarks/wintercg/throughput.sx +++ b/benchmarks/wintertc/throughput.sx @@ -15,3 +15,17 @@ ee.on("x", (v) => { count += v; }); t0 = performance.now(); for (let i = 0; i < N; i++) ee.emit("x", i); console.log("events:", (performance.now() - t0).toFixed(1), "ms"); + +// JSON: a 1MB API payload, parsed and written back out. This is the shape of +// a request body in any JSON API, and both directions are under test. +const doc = JSON.stringify(Array.from({ length: 2000 }, (_, i) => ({ + id: i, + name: "record " + i, + tag: "t" + (i % 7), + bio: "the quick brown fox jumps over the lazy dog ".repeat(6), + ok: i % 2 === 0, + score: i * 3, +}))); +t0 = performance.now(); total = 0; +for (let i = 0; i < 40; i++) total += JSON.stringify(JSON.parse(doc)).length; +console.log("json:", (performance.now() - t0).toFixed(1), "ms"); diff --git a/benchmarks/wintercg/timeone.py b/benchmarks/wintertc/timeone.py similarity index 100% rename from benchmarks/wintercg/timeone.py rename to benchmarks/wintertc/timeone.py diff --git a/docs/_page.html b/docs/_page.html index 8302700..f9bfd13 100644 --- a/docs/_page.html +++ b/docs/_page.html @@ -111,6 +111,35 @@ } .doc-nav a.next { text-align: right; } .doc-source { margin-top: 26px; font-size: 13px; color: var(--ink-soft); } + + /* ---- page menu: the same page as markdown, for a person or an agent ---- */ + .page-menu { position: relative; display: flex; justify-content: flex-end; margin-bottom: -8px; } + .page-menu > button { + display: inline-flex; align-items: center; gap: 7px; font: inherit; font-size: 13px; + font-weight: 600; color: var(--ink-mid); background: var(--card); + border: 1px solid var(--rule); border-radius: 8px; padding: 6px 11px; cursor: pointer; + } + .page-menu > button:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--rule)); color: var(--ink); } + .page-menu > button::after { + content: ""; width: 13px; height: 13px; background: var(--ink-soft); + -webkit-mask: url('data:image/svg+xml;utf8,') center / contain no-repeat; + mask: url('data:image/svg+xml;utf8,') center / contain no-repeat; + } + .page-menu ul { + position: absolute; top: calc(100% + 6px); right: 0; z-index: 20; min-width: 232px; + margin: 0; padding: 5px; list-style: none; background: var(--card); + border: 1px solid var(--rule); border-radius: 10px; + box-shadow: 0 12px 30px -12px rgb(0 0 0 / .3); + } + .page-menu[hidden] ul, .page-menu ul[hidden] { display: none; } + .page-menu li a, .page-menu li button { + display: block; width: 100%; text-align: left; font: inherit; font-size: 13.5px; + color: var(--ink); text-decoration: none; background: none; border: 0; border-radius: 7px; + padding: 7px 10px; cursor: pointer; + } + .page-menu li a:hover, .page-menu li button:hover { background: var(--rule); } + .page-menu li span { display: block; font-size: 11.5px; color: var(--ink-soft); margin-top: 1px; } + .page-menu hr { border: 0; border-top: 1px solid var(--rule); margin: 5px 2px; } @@ -120,6 +149,7 @@ SxfeScript diff --git a/docs/release-notes-0.0.2.md b/docs/release-notes-0.0.2.md new file mode 100644 index 0000000..62650aa --- /dev/null +++ b/docs/release-notes-0.0.2.md @@ -0,0 +1,45 @@ +# SxfeScript 0.0.2 + +0.0.2 is the first release with the current SxfeScript/ArcSX runtime surface, +the expanded Node-compatible API, and release binaries for all six supported +platform/architecture combinations. + +## Highlights + +- Added the documented `Sxn.serve` Request/Response API, including keep-alive, + request-body bytes, response headers, binding, and `reusePort` support. +- Completed the current minimum Common API and added the remaining registered + `node:` modules described by the runtime documentation. +- Moved high-frequency Buffer, crypto, URL, filesystem, stream, formatting, + and inspection paths into native C implementations where profiling showed a + measurable gain. +- Improved JSON parsing/serialization, including word-at-a-time scanning, + lower allocation overhead, cycle handling, and numeric formatting. +- Enforced `&mut` ownership checks and compiled declared scalar types instead + of treating all annotations as comments. +- Added idle cycle sweeping and EventEmitter listener-limit diagnostics. +- Added the browsable guides, runnable examples, Windows PowerShell installer, + and machine-readable `llm.txt`/`llms.txt` release metadata. + +## Performance snapshot + +The fresh Linux HTTP run used an AMD Ryzen 7 5700G, 16 threads, Node v23.11.1, +Go 1.26, bombardier, 100,000 requests, and 125 connections per row: + +| Test | ExpressX (16 processes) | Winner | +|---|---:|---| +| Static | 165,633 req/s | ExpressX | +| Parameterized | 145,742 req/s | Go net/http (150,195) | +| REST (1 MB JSON POST) | 798 req/s | ExpressX | + +ExpressX's single-process rows were 35,404 / 33,066 / 232 req/s for the same +tests. Results vary with machine load; compare rows within one run. The full +report and raw data are maintained in the ExpressX benchmark repository. + +## Builds and verification + +Release archives are provided for macOS arm64/x64, Linux arm64/x64, and Windows +arm64/x64. macOS and Linux builds pass all 102 CTest cases. Windows binaries +are cross-compiled and verified as PE32+ for their advertised architecture +and as embedding `sxn 0.0.2`; they were not executed on Windows hardware in +this release workspace. diff --git a/include/sxfe.h b/include/sxfe.h index e2c200b..723b6ef 100644 --- a/include/sxfe.h +++ b/include/sxfe.h @@ -9,7 +9,7 @@ extern "C" { #endif -#define SXFE_VERSION "0.0.1" +#define SXFE_VERSION "0.0.2" /* Length of the absolute-path root at p: 0 if relative, 1 for POSIX's leading '/', or 3 for a Windows drive letter (`C:/` or `C:\`). Used by @@ -106,14 +106,31 @@ int sxn_install_network(struct JSContext *context); calls Sxn.serve or the async file API). */ int sxn_run_event_loop(struct JSContext *context); +/* Idle cycle sweeping, configured by the CLI before the loop starts. + `enabled` false turns it off entirely (`--no-idle-gc`); `floor_bytes` is + the tracked-allocation size below which the loop will not spend a + collection (`--idle-gc-floor=`), so a small server keeps the pause + profile it has today. Call before sxn_run_event_loop; the defaults apply + if it is never called. */ +void sxn_configure_idle_gc(int enabled, size_t floor_bytes); + /* Installs the `node:buffer`/`node:path`/`node:events`/`node:process` compatibility modules (src/node.c + src/node_compat.js), following the same native-primitives-plus-JS-bootstrap split as sxn_install_network. exec_path becomes process.argv[0]. */ int sxn_install_node_compat(struct JSContext *context, const char *exec_path); +/* Registers one node: builtin module by its full specifier ("node:fs"), or + returns NULL if this runtime has no such module. Called by the module + loader, so that a program pays only for the builtins it imports. */ +struct JSModuleDef *sxn_node_module_load(struct JSContext *context, const char *name); /* Releases the atoms sxn_install_node_compat cached; call once, before JS_FreeContext, or the runtime reports them as leaked. */ void sxn_free_node_compat(struct JSContext *context); +/* Drops the emit memo's strong references to one emitter's `_events` object, + event name and listener list. A cycle sweep cannot see past them, so both + Sxn.gc() and the idle sweep release the memo first. Safe to call at any + time: the memo is a cache and the next emit rebuilds it. */ +void sxn_free_ee_memo(struct JSContext *context); #ifdef __cplusplus } diff --git a/llm.txt b/llm.txt new file mode 100644 index 0000000..ee5b78a --- /dev/null +++ b/llm.txt @@ -0,0 +1,18 @@ +# SXN release notes for language models + +Current release: 0.0.2. + +Six verified release binaries are built and packaged under `dist/0.0.2/`: + +- macOS arm64 and x86_64 (Mach-O) +- Linux arm64 and x86_64 (ELF) +- Windows arm64 and x86_64 (PE32+) + +The macOS and Linux builds pass all 102 CTest cases. Windows artifacts are +cross-compiled and verified for PE architecture and embedded `sxn 0.0.2`, but +are not execution-tested on Windows hardware in this workspace. + +For release work, `scripts/release.sh` builds the native/cross Unix targets +and uploads archives. Unix targets use `.tar.gz`; Windows installers use +`.zip` (the PowerShell installer expects that format). Prebuilt Windows +archives must be placed in `dist/`. diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..9e1c6fa --- /dev/null +++ b/llms.txt @@ -0,0 +1,5 @@ +# SxfeScript / SXN + +The current release is 0.0.2. See `llm.txt` for the verified six-platform +binary matrix, build provenance, and test status. The source documentation is +in `README.md`, `docs/`, and `spec/`. diff --git a/scripts/build-docs.py b/scripts/build-docs.py index 4e5392d..dfa9f29 100755 --- a/scripts/build-docs.py +++ b/scripts/build-docs.py @@ -29,32 +29,52 @@ SITE = "https://sxfescript.github.io" # (source file, url slug, title, sidebar section, one-line description) +# +# Order is the reading order, and the first four are the path a newcomer takes: +# why you would use this, how to install it, how to run something, what it +# looks like. The specs are still the source of truth and still every one of +# them is on the site -- they sit under Reference and Project, behind the +# guides, rather than being the first documentation page anyone meets. PAGES = [ + ("docs/guide/why.md", "why", "Why sxn", "Get started", + "What sxn is, how it compares to Node and Bun, and when to pick something else."), + ("docs/guide/install.md", "install", "Install", "Get started", + "One command on macOS, Linux and Windows, or a build from source."), ("docs/guide/quickstart.md", "quickstart", "Quick start", "Get started", - "Install sxn, run your first .js, .ts and .sx file, and start an HTTP server."), + "Run your first file, borrow a value, and start an HTTP server."), ("docs/guide/examples.md", "examples", "Examples", "Get started", "Complete programs you can run, each with the output it actually prints."), - ("spec/CLI.md", "cli", "CLI reference", "Get started", - "Every sxn command and flag, and how a file with no extension is resolved."), - ("spec/LANGUAGE.md", "language", "Language contract", "The language", - "What .sx adds to JavaScript: erasable types, safe/unsafe, ownership, layout."), - ("spec/ABI.md", "abi", "ABI", "The language", - "The boundary between SxfeScript values and native memory."), - ("spec/BYTECODE.md", "bytecode", "Bytecode", "The language", - "Compiling to .sxbc, the compile cache, the measured gains, and the trust boundary."), - - ("spec/RUNTIME.md", "runtime", "Runtime surface", "The runtime", - "The WinterCG web APIs and the Sxn host namespace: fetch, Sxn.serve, streams, crypto, FFI."), - ("spec/NODE.md", "node", "Node compatibility", "The runtime", + ("docs/guide/http-server.md", "http-server", "An HTTP server", "Guides", + "Sxn.serve: request bodies, routing, keep-alive, and node:http alongside it."), + ("docs/guide/node-packages.md", "node-packages", "Using node: packages", "Guides", + "Running an existing Node project: resolution, the builtins, addons, and the gaps."), + ("docs/guide/types.md", "types", "Types and .sx", "Guides", + "TypeScript with no build step, and what a declared type does that TypeScript's does not."), + ("docs/guide/ownership.md", "ownership", "Ownership and borrows", "Guides", + "let mut, & and &mut, the one rule enforced today, and fixed-layout structs."), + ("docs/guide/benchmarks.md", "benchmarks", "Benchmarks", "Guides", + "sxn against Node and Bun on two machines, and how to run the suite yourself."), + + ("spec/CLI.md", "cli", "CLI reference", "Reference", + "Every sxn command and flag, and how a file with no extension is resolved."), + ("spec/RUNTIME.md", "runtime", "Runtime surface", "Reference", + "The WinterTC web APIs and the Sxn host namespace: fetch, Sxn.serve, streams, crypto, FFI."), + ("spec/NODE.md", "node", "Node compatibility", "Reference", "CommonJS, the node: builtins, and .node native addons through a from-scratch Node-API."), - ("spec/NATIVE.md", "native", "Native code", "The runtime", + ("spec/LANGUAGE.md", "language", "Language contract", "Reference", + "The normative rules: erasable types, safe/unsafe, ownership, layout."), + ("spec/NATIVE.md", "native", "Calling C", "Reference", "Sxn.ffi and .node addons, and why only one of the two belongs to the engine."), - ("spec/NETWORK.md", "network", "Networking", "The runtime", + ("spec/BYTECODE.md", "bytecode", "Compiling to bytecode", "Reference", + "Compiling to .sxbc, the compile cache, the measured gains, and the trust boundary."), + ("spec/ABI.md", "abi", "ABI", "Reference", + "The boundary between SxfeScript values and native memory."), + ("spec/NETWORK.md", "network", "Networking", "Reference", "The networking primitives the runtime is built on."), - ("spec/PERFORMANCE.md", "performance", "Performance", "Project", - "The full benchmark write-up: methodology, both machines, and every optimization behind the numbers."), + ("spec/PERFORMANCE.md", "performance", "Performance notes", "Project", + "Every optimization behind the numbers, the ceilings that measured zero, and what is open."), ("spec/BENCHMARK_REFERENCES.md", "benchmark-references", "Benchmark references", "Project", "Where the comparison numbers come from."), ("spec/IMPLEMENTATION.md", "implementation", "Implementation ledger", "Project", @@ -84,6 +104,64 @@ def one(m): return INCLUDE_RE.sub(one, text) +SECTION_RE = re.compile(r"^$", re.M) + + +def expand_section_includes(text): + """`` becomes + that heading's block, heading line included, up to the next heading of the + same or a higher level -- or, for the last heading in a file, to the end of + it, trailing prose and all. Two optional trailing keywords narrow that: + + - `table` takes just the table rows. + - `body` drops the heading line and keeps the rest, for a page supplying + its own heading above the include. + + Both exist for the same reason: the commentary around a table or section in + its home document is written for a reader who has that whole document, and + reappearing verbatim in a page that goes on to make its own point about the + same material reads as a dangling non sequitur -- doubly so for a raw + `path/to/File.md` that was a working link in its own context and is inert + code text here, and for a second, near-identical heading directly under the + including page's own. + + Same reason as the code include above, applied to prose: the benchmark + tables belong on the landing page, on the benchmarks page and in the + README, and three hand-kept copies of a table of measured numbers is three + chances to publish a figure that is no longer true.""" + + def one(m): + source = (ROOT / m.group(1)).read_text() + want = m.group(2) + lines = source.splitlines() + start = level = None + block = None + for i, line in enumerate(lines): + heading = re.match(r"^(#{1,6}) +(.*)$", line) + if not heading: + continue + if start is None: + if slugify(heading.group(2)) == want: + start, level = i, len(heading.group(1)) + elif len(heading.group(1)) <= level: + block = lines[start:i] + break + if start is None: + sys.exit(f"build-docs: no section #{want} in {m.group(1)}") + if block is None: + block = lines[start:] + if m.group(3) == "table": + rows = [l for l in block if l.startswith("|")] + if not rows: + sys.exit(f"build-docs: no table in {m.group(1)}#{want}") + return "\n".join(rows) + if m.group(3) == "body": + block = block[1:] + return "\n".join(block).strip("\n") + + return SECTION_RE.sub(one, text) + + # GitHub has no highlighter for .sx, and an ```sx fence comes back as flat # unhighlighted text. TypeScript's covers the language almost exactly -- it is # JavaScript plus the same annotation syntax -- so the fence is relabelled on @@ -171,11 +249,48 @@ def one(m): return re.sub(r'href="([^"]*)"', one, body) +SLUGS = {slug for _, slug, _, _, _ in PAGES} +MD_LINK_RE = re.compile(r"\]\(([^)\s]+)\)") + + +def rewrite_md_links(text, repo_url): + """The .md twin is read from docs/.md while the page it mirrors is at + docs//, so every relative link in it is off by one directory. Make + them absolute instead, pointing at the other twins: something that fetched + this file should be able to follow a link without knowing where it came + from.""" + + def one(m): + href = m.group(1) + if href.startswith(("http://", "https://", "#", "mailto:")): + return m.group(0) + target, _, fragment = href.partition("#") + target = target.strip("./") + if target in BY_SOURCE: + new = f"{SITE}/docs/{BY_SOURCE[target][0]}.md" + elif target in SLUGS: + new = f"{SITE}/docs/{target}.md" + elif target in ("docs/index.html", "docs", ""): + new = SITE + "/" + elif target: + new = f"{repo_url}/blob/main/{target}" + else: + return m.group(0) + return f"]({new}#{fragment})" if fragment else f"]({new})" + + return MD_LINK_RE.sub(one, text) + + SOURCE_RE = re.compile(r"") # An inline snippet that is an illustration rather than a runnable file -- the # hero's TypeScript-versus-SxfeScript pair. Same renderer, so the hero and the # example panels below it are highlighted by one thing rather than two. INLINE_RE = re.compile(r"(.*?)", re.S) +# The landing page's copy of a markdown section -- the benchmark tables. The +# optional `table` keyword takes the table alone and leaves the section's prose +# behind: the README's commentary around a table is written for a reader who +# has the whole document, and reads as a non-sequitur on a landing page. +SECTION_HTML_RE = re.compile(r"") def render_index(repo_url): @@ -200,8 +315,25 @@ def inline(m): rendered = render_markdown(f"```{m.group(1)}\n{snippet}\n```") return retag_sx_keywords(rendered).strip() + def section(m): + """A markdown section rendered into the landing page, so the benchmark + tables have one source rather than a copy here and a copy on the + benchmarks page.""" + suffix = f" {m.group(3)}" if m.group(3) else "" + text = expand_section_includes(f"") + # The heading line belongs to the page's own layout, not the include -- + # already gone when `table` filtered down to just the rows. + text = re.sub(r"^#{1,6} +.*\n", "", text, count=1) + # `scroll` rather than the doc pages' `table-scroll`: the landing page + # has its own stylesheet and that is the class it defines. + rendered = render_markdown(text.strip()) + rendered = (rendered.replace("", '
') + .replace("
", "")) + return rendered.strip() + page = SOURCE_RE.sub(one, page) page = INLINE_RE.sub(inline, page) + page = SECTION_HTML_RE.sub(section, page) return page.replace("{{REPO_URL}}", repo_url) @@ -240,7 +372,7 @@ def build(out_dir, repo_url): rendered = [] for index, (source, slug, title, _, description) in enumerate(PAGES): - text = expand_includes((ROOT / source).read_text()) + text = expand_section_includes(expand_includes((ROOT / source).read_text())) # Depth from /docs// back to the site root. root = "../../" body = retag_sx_keywords(render_markdown(text)) @@ -253,14 +385,21 @@ def build(out_dir, repo_url): .replace("{{SIDEBAR}}", sidebar_html(slug, root)) .replace("{{PREVNEXT}}", prevnext_html(index, root)) .replace("{{CONTENT}}", body) + .replace("{{SLUG}}", slug) .replace("{{SOURCE}}", source) .replace("{{ROOT}}", root) .replace("{{REPO_URL}}", repo_url)) target = out_dir / "docs" / slug target.mkdir(parents=True, exist_ok=True) (target / "index.html").write_text(page) + # The same page as markdown, at docs/.md. Bun and Lynx both do + # this and it is what the Copy page / View as Markdown / Open in + # controls in _page.html point at: a model handed a link + # should land on the source text, not on a page to scrape. GitHub + # Pages serves .md as text/markdown with no configuration. + (out_dir / "docs" / f"{slug}.md").write_text(rewrite_md_links(text, repo_url)) rendered.append((source, slug, title, description, text)) - print(f" docs/{slug}/", file=sys.stderr) + print(f" docs/{slug}/ + docs/{slug}.md", file=sys.stderr) (out_dir / "index.html").write_text(render_index(repo_url)) print(" index.html", file=sys.stderr) @@ -279,16 +418,25 @@ def write_llms(out_dir, rendered): lines = [ "# SxfeScript and SXN", "", - "> SxfeScript is JavaScript with an explicit safe/unsafe boundary, affine " - "values and lexical borrows, plus erasable TypeScript annotations, all parsed " - "natively with no build step. It runs on ArcSX, a QuickJS-based runtime built " - "as `sxn` that is designed to run the same on a phone as it does on a server.", + "> `sxn` is a JavaScript runtime. It runs .js, .mjs, .cjs, .ts and .sx files " + "directly with no build step, resolves node_modules the way Node does, " + "implements the node: builtins, and serves HTTP. It cold-starts in 8.4 ms " + "against Node's 41.6 on the same machine, and its worst GC pause is 0.04 ms " + "against Node's 0.36 and Bun's 2.59.", "", - "ArcSX implements the WinterCG web APIs (fetch, Sxn.serve, Web Streams, Web " - "Crypto) and a Node compatibility layer (CommonJS, node: builtins, .node " - "native addons). It has no JIT, deliberately: iOS will not grant a " - "third-party app the entitlement to generate machine code, and running there " - "is the point.", + "It has no JIT, deliberately: iOS will not grant a third-party app the " + "entitlement to generate machine code, so the runtime is built to be fast " + "without one and the same binary behaves the same on a phone and a server. " + "The tradeoff is real -- on a JIT-bound numeric loop, Node and Bun win. " + "The engine is ArcSX, a QuickJS fork; it implements the WinterTC Minimum " + "Common API (fetch, Sxn.serve, Web Streams, Web Crypto) and a Node " + "compatibility layer including .node native addons.", + "", + "SxfeScript (.sx) is the optional language half: JavaScript with mutation and " + "aliasing made explicit (`let mut`, `&`, `&mut`) and TypeScript-style " + "annotations that are compiled rather than erased -- a declared i32 wraps at " + "the 32-bit boundary, and a small function with a declared scalar signature is " + "inlined into its caller.", "", f"- Source: {SITE.replace('sxfescript.github.io', 'github.com/SxfeScript/sxfescript')}", f"- Every page below, in full: {SITE}/llms-full.txt", @@ -303,10 +451,13 @@ def write_llms(out_dir, rendered): lines.append(f"## {group}") lines.append("") section = group - lines.append(f"- [{title}]({SITE}/docs/{slug}/): {description}") + # Link the markdown twin, not the HTML page: a model that follows one + # of these should get the source text rather than a rendered page. + lines.append(f"- [{title}]({SITE}/docs/{slug}.md): {description}") lines.append("") (out_dir / "llms.txt").write_text("\n".join(lines)) + full = [ "# SxfeScript and SXN, complete documentation", "", diff --git a/scripts/release.sh b/scripts/release.sh index c4e2bba..bbead01 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash -# Builds sxn for every target this session can reach locally (macOS -# arm64/x64, Linux arm64/x64) and uploads them to a GitHub release. -# Windows isn't built here - there's no Windows machine in this setup, so -# it stays CI's job (see .github/workflows/ci.yml); this script only -# covers what these two machines can actually build and test themselves. +# Builds and packages the macOS/Linux targets reachable from this host and +# uploads any matching prebuilt Windows archives supplied in dist/. Windows +# x64/arm64 are cross-built separately; keep their binaries in dist/ before +# invoking this script so a release contains all six targets. # # Usage: scripts/release.sh vX.Y.Z [--prerelease] # @@ -97,7 +96,7 @@ else fi echo "== Uploading to $REPO release $VERSION ==" -assets=(dist/sxn-"${VERSION#v}"-*.tar.gz) +assets=(dist/sxn-"${VERSION#v}"-*.tar.gz dist/sxn-"${VERSION#v}"-*.zip) if gh release view "$VERSION" --repo "$REPO" >/dev/null 2>&1; then gh release upload "$VERSION" "${assets[@]}" --repo "$REPO" --clobber else diff --git a/spec/ABI.md b/spec/ABI.md index 3992042..815dec2 100644 --- a/spec/ABI.md +++ b/spec/ABI.md @@ -9,6 +9,14 @@ The production engine will expose distinct `sx_*` bytecodes for allocation, move, shared borrow, mutable borrow, release, field access, and owned drop. QuickJS `OP_drop` remains untouched because it is an operand-stack operation. +None of those exist yet, and the ownership rules are still erased at runtime: +`&mut` aliases through JavaScript object identity, so it mutates a struct in +place but cannot write back to a caller's number. One rule is enforced ahead +of them at parse time -- `&mut` requires a `let mut` owner, SX2003 -- and the +declared scalar types are recorded rather than stripped, which is what gates +the i32 arithmetic semantics and the typed-call inlining. `spec/LANGUAGE.md` +has the contract and `spec/IMPLEMENTATION.md` has what is checked today. + Moving a layout value into JavaScript consumes it and boxes a copy. Borrowing it into JavaScript creates a revocable proxy. Typed JavaScript objects crossing into SX use shared/exclusive header locks, and incompatible property writes diff --git a/spec/BYTECODE.md b/spec/BYTECODE.md index 7996b88..cdceb4d 100644 --- a/spec/BYTECODE.md +++ b/spec/BYTECODE.md @@ -16,7 +16,14 @@ mechanism: something a user's own script can opt into. Both produce and consume the same file format, so `sxn app.sxbc` runs either -one's output directly. +one's output directly. A module's imports are resolved while it is compiled, +so compiling a file compiles what it imports; the bytecode holds the entry +module, and the imports are resolved by name when it runs. + +What it does not do is make a running program faster. The interpreter +executes the same bytecode either way -- compiling ahead of time removes the +parse, which happens once. On a server the difference is a millisecond of +startup against however long the server then runs. ## Is it worth it for your script? diff --git a/spec/CLI.md b/spec/CLI.md index 5559334..dfea829 100644 --- a/spec/CLI.md +++ b/spec/CLI.md @@ -4,6 +4,16 @@ - `sxn [--memory-report] [--leak-check] [--compile-cache] [args...]` runs a file with diagnostics on, or (`--compile-cache`) via a bytecode cache built and reused across launches -- see `spec/BYTECODE.md`. +- `sxn [--no-idle-gc] [--idle-gc-floor=] [args...]` tunes cycle + sweeping while the event loop is quiet. A long-running server that takes a + burst leaving reference cycles behind and then waits would otherwise hold + them: the collector runs only on allocation, and the collection at the + burst's peak has already raised its threshold above what leaked. So the loop + sweeps when it has been blocked waiting, has not swept in the last half + second, and is holding more than the floor -- 32 MB by default, below which + nothing is swept and a small server keeps exactly the pause profile it had. + `--no-idle-gc` turns it off entirely. `Sxn.gc()` is the explicit ask, + independent of both. - `sxn compile [-o out.sxbc] [--strip]` compiles a file to bytecode for distribution, without running it. `spec/BYTECODE.md`. - `sxn run [script] -- [args...]` executes a `package.json` script. diff --git a/spec/IMPLEMENTATION.md b/spec/IMPLEMENTATION.md index 70b4cb6..65212bf 100644 --- a/spec/IMPLEMENTATION.md +++ b/spec/IMPLEMENTATION.md @@ -11,6 +11,53 @@ separate transform step. The earlier in-memory text transformer (`sxfe_compile`, src/frontend.c) remains as an independently unit-tested component but is no longer on the execution path. +- One ownership rule is enforced rather than erased: `&mut x` requires `x` to + be a `let mut` owner, and borrowing an immutable one is a parse error naming + SX2003 (`js_parse_unary`, third_party/quickjs/quickjs.c). The check rules + only on a bare identifier it can resolve in the current function's lexical + scope chain or in its top-level lexicals; a parameter, a captured outer + binding, or anything it cannot resolve is left alone rather than guessed at. + Guarded by the `sxn-reject-mut-borrow` ctest. Note that `&mut` is still + erased at runtime, so it aliases through JS object identity and cannot write + back to a caller's number -- that needs the ownership CFG below. +- Declared types are recorded rather than discarded. + `js_parse_type_annotation` classifies while it skips, and the result is kept + on `JSVarDef.sx_type` -- which covers parameters too, since `fd->args` is a + `JSVarDef[]` and reaches the bytecode through the `vardefs` memcpy -- and as + `JSFunctionBytecode.sx_ret_type`. Only the scalar types codegen acts on are + named. A union, a generic, an inline object type, a borrow, a struct name, + `string` and `void` all report `SX_TYPE_OTHER`, so codegen never specializes + on a type it half-understood, and no enum value exists that nothing reads. + The type is deliberately not propagated to `JSGlobalVar` or `JSClosureVar`: + both were written and never read, and a `safe` module-level binding is kept + in `fd->vars` by `sx_safe_module_local` anyway. The skipping itself is + unchanged, which is what keeps arbitrary erasable TypeScript parsing. +- `safe` specializes on the type that was written, not on the keyword. The i32 + opcodes are gated on `is_safe && sx_type == SX_TYPE_I32` + (`sx_is_safe_i32`), so `safe let mut x: f64` and an un-annotated `safe let + mut` keep exact JavaScript arithmetic instead of reaching a wrapping integer + opcode and being rescued by its runtime tag test. This also settled an + inconsistency: a constant right-hand side and a local one compile through + different peephole branches, and `safe let mut n: i32` used to promote on + `n += 1` while wrapping on `n += step`. Both wrap now. + `tests/fixtures/typed_overflow.sx` covers all four combinations plus the + fused loop; `tests/fixtures/js_overflow.mjs` stays the plain-JS boundary. +- Typed calls are inlined. A call whose callee is a local written exactly + once by an `fclosure`, never captured, with every parameter and the return + declared scalar, and whose body loads each parameter once in order and then + runs only operand-free arithmetic, is spliced into the caller at the pass-2 + peephole (`sx_inline_typed_calls`). The measured effect is below. Plain + JavaScript never qualifies, because the gate is the declared signature. + `tests/fixtures/typed_inline.sx` asserts the splice changes nothing + observable -- values, coercions, exceptions, reassignment, capture, wrong + arity, use as a value -- with every expected value taken from Node. +- Cycle sweeping when the event loop is quiet, plus `Sxn.gc()` and an `rss` + field on `Sxn.memoryUsage()`. Why it was needed, and what it is worth, is + below. +- `EventEmitter` warns once per emitter and event when a listener count + crosses its limit, with Node's `MaxListenersExceededWarning` object and + wording, and `setMaxListeners`/`getMaxListeners` to raise or disable it. + `defaultMaxListeners` had been a constant nothing read. - Fixed-layout calculation and aligned growable/poisonable arena primitives. - Module-loader hook that transforms imported `.sx` modules in memory. - Package command surface with safe argument validation, disabled lifecycle @@ -37,7 +84,7 @@ generated header; `third_party/QUICKJS-PROVENANCE.md` has the lineage this is built on. -## Performance shape (measured, benchmarks/wintercg/run.sh) +## Performance shape (measured, benchmarks/wintertc/run.sh) - sxn wins seven of the eight README benchmark categories on both measured machines: startup, cold start, Buffer and TextEncoder throughput, both @@ -127,6 +174,80 @@ ns, because the extra branch on every cache hit costs more than the walk it saves), and a single-way per-call-site inline cache (12% slower than shape keying on a four-shape call site). +### The call frame is the one large removable cost, and types remove it + +Every ceiling above measured at roughly zero. This one did not. All figures +are the minimum of 9 runs over 5M iterations, macOS arm64, Release, reported +per loop iteration; subtract the empty loop to read the work itself. + +| | sxn | Node 25.2 | Bun 1.2.17 | +|---|---|---|---| +| empty loop | 4.0 ns | 0.27 | 0.23 | +| interpreted call, 0 args | 14.4 ns | -- | -- | +| interpreted call, 2 args | 16.3 ns | 0.37 | 0.22 | +| interpreted call, 4 args | 21.3 ns | -- | -- | +| field read + write | 11.9 ns | 0.27 | 0.22 | +| f64 accumulate | 5.9 ns | 0.53 | 0.54 | + +Read those the way the ledger reads every JIT comparison: Node and Bun delete +these loops outright, so no interpreter change reaches 0.3 ns. What the table +locates is where *this* runtime's time goes. A call costs ~10.4 ns before an +argument is passed and ~1.7 ns per argument after; a field access is ~2.6 ns; +arithmetic is already within 2 ns of the dispatch floor and has nothing left +in it. Annotations bought none of this before: typed and untyped field access +measured 11.76 and 11.67 ns, the same number. + +Hand-inlining gave the exact upper bound for removing the frame: + +| | via call | hand-inlined | recovered | +|---|---|---|---| +| `add2(i, 1)`, both `i32` | 17.1 ns | 5.4 | 11.8, 3.2x | +| `len2(v)`, an interface | 28.9 ns | 18.1 | 10.7, 1.6x | + +**A cheaper prologue is a negative result.** Before building one, the prologue +was ablated: `-DSXN_ABLATE_CALL_PROLOGUE` removes the stack-overflow check and +the GC-free section, the only two pieces a statically known callee could be +proven not to need. Interleaved over three rounds the 0-argument call went +14.39 -> 12.89 ns and both 2-argument rows landed inside noise. A 1.5 ns +ceiling on the one shape that benefits does not justify branching +`JS_CallInternal` on a signature, so it was closed rather than written. The +rest of the prologue is not ablatable at all -- the `var_buf` fill, the +`var_refs` fill and the realm switch change meaning rather than repeat known +work, and a build without them crashes during bootstrap. The flag stays in +the source so the result can be re-derived on another target. + +**Inlining is where the 11 ns was, and it needs no opcode.** +`sx_inline_typed_calls` runs in the pass-2 peephole beside +`fuse_i32_accum_loops` and splices the callee's body over the call, which +matters because the opcode space is full (`static_assert(OP_COUNT == 256)`; +`spec/PERFORMANCE.md` records the template-literal `concat` op taking the last +slot). Measured with `benchmarks/engine/call_inline_probe.sx`: + +| | ns/op | +|---|---| +| empty loop | 4.07 | +| untyped call | 16.34 | +| typed call, inlined | 5.31 | +| hand-inlined ceiling | 5.38 | + +16.34 -> 5.31 ns, 3.1x, landing on the hand-inlined ceiling. Against Node's +0.37 ns for the same call that is a 44x gap narrowed to 14x -- narrowed, not +closed, and the row should be quoted that way. Compile time did not move: +20000 small functions still compile in 40 ms, and the pass early-outs on any +function with no locals or no constant pool. + +The `benchmarks/wintertc` rows are unchanged by all of it (buffer 19.4 ms, +textencoder 4.6, events 6.7, worst pause 0.04), which is expected: none of +those workloads calls a small function with a declared scalar signature. + +Two limits worth stating rather than discovering later. The splice requires +the body to load every parameter once in declaration order, so +`(a, b) => a * b + c` and anything reusing a parameter (`v.x * v.x`) still +pays for its frame; lifting that needs the arguments in caller temporaries, +which means allocating locals after `resolve_variables`. And an inlined callee +no longer appears in a stack trace if its arithmetic throws -- the same +tradeoff every inlining compiler makes. + ### A JIT is ruled out by platform, not by effort iOS does not grant W^X/JIT entitlements to third-party apps, so a @@ -138,6 +259,86 @@ nothing faster exists. Benchmarks against JIT runtimes should be read with that in mind: on JIT-bound microbenchmarks the comparison is against a technique this project won't use, not necessarily a result it can't reach. +### An idle process never collected, and the leak protected itself + +This one is a correctness result rather than a speed one, and it is the +counterpart to the two negative results below: the collector's *cadence* +turned out to be worth far more than its *mechanism*. + +Before this change, nothing in `src/` ever called `JS_RunGC`. The only +GC-related line in the whole project was `JS_SetGCThreshold(runtime, 8 MB)` in +`src/main.c`. Collection happened solely inside `js_trigger_gc`, when an +allocation would cross the threshold -- and that function then sets the next +threshold to `malloc_size + (malloc_size >> 1)`, 1.5x whatever was live. + +Those two facts combine badly. Measured with `Sxn.memoryUsage()`, building +200,000 objects that each hold `self` and a closure reaching back into them, +holding the batch, then dropping it: + +| | tracked bytes | collections | +|---|---|---| +| start | 734,672 | 0 | +| after the same burst built **acyclic** | 735,584 | 8 | +| after the **cyclic** burst is dropped | 229,535,648 | 9 | +| after 12 further rounds of ~8 MB churn | 246,984,720 | **9** | + +Peak RSS 251 MB. The last two rows are the finding: 96 MB of subsequent +allocation and release produced *zero* collections. The collection at the peak +correctly sized the threshold for a 229 MB live set; the burst then died, but +nothing re-evaluates a threshold except another collection, and the later +churn is freed by refcounting as it goes so the bar is never reached again. +**The larger the cyclic garbage, the higher the bar for collecting it**, and +each burst raises a floor that never comes down. + +Three things it is *not*, each checked rather than assumed. Acyclic garbage is +freed immediately by refcounting (row two). Cycles under sustained load are +collected fine -- 500,000 cyclic closures in a tight loop fired 708 +collections and finished at 743 KB against a 736 KB baseline, and a +3,000-request `Sxn.serve` run creating a cycle per request stayed at 787 KB. +And the connection and callback registries do not retain: `ConnState` is a C +list unlinked on close and is deliberately passed through its promise +continuations *as an integer* rather than a JS value, `SxnTimer` is unlinked +in `sxn_timer_stop`, and the fetch, chunk-view and UDP states each hold their +callbacks in a C struct with a matching finalizer. Only quiescence after a +peak leaks. + +The fix is `sxn_maybe_idle_gc`, called from both loops in `src/network.c` -- +`sxn_run_event_loop` and `sxn_await_with_loop`, because a module with +top-level await never reaches the first, and hooking only it left every +`await` daemon uncollected. It sweeps when the previous `uv_run` blocked for +20 ms or more, tracked size is above a 32 MB floor, and half a second has +passed since the last sweep. It then resets the threshold, which is the +load-bearing half: `JS_RunGC` reclaims without touching it, so a sweep that +does not also reset leaves the ratchet in place for the next burst. + +The first attempt gated on tracked size not growing between turns and never +fired at all -- any loop with a timer on it allocates a trickle every turn, so +the size always crept up. Timing how long `uv_run` blocked is the direct +measurement and is what shipped. + +Results. A burst-then-idle daemon now reclaims 171 MB across one loop turn +with nothing calling anything (`tests/fixtures/leak_idle_sweep.mjs`), and +`Sxn.gc()` takes 229.5 MB back to 735 KB explicitly. A server under sustained +load takes *no* sweep at all -- 4,000 requests, zero collections, and the same +wall time with the feature on and off -- because `uv_run` never blocks long +enough to open the gate. The benchmark rows are unmoved: buffer 18.9 ms, +textencoder 4.8, events 6.5, worst pause 0.04 ms, all matching the figures +above and matching a `--no-idle-gc` run. + +`benchmarks/engine/gc_idle_probe.sx` re-derives the whole thing and reports +RSS beside tracked bytes. Note the two diverge exactly as expected: after a +sweep that took tracked bytes from 164 MB to 0.7 MB, RSS only moved 199.8 -> +196.8 MB, because the system allocator kept the pages. That is why the +fixtures assert on `mallocSize` and only report `rss`. + +Two items proposed alongside this were not built. Reworking the network +registries onto `WeakRef` has nothing to attach to, per the audit above. +Statically flagging non-escaping values to skip cycle-collector registration +is the `-DSXN_ABLATE_GC_LIST` experiment below, whose exact upper bound is +0-1 ns -- and unregistering a value that can still enter a cycle is how a +collector frees something reachable, so the risk is not proportionate to a +measured zero. + ### The remaining collector rewrites are measured at ~zero ceiling The second-opinion review (see below) rated two collector-level designs as @@ -221,7 +422,7 @@ The remaining item is a generational nursery for object churn refcounting versus a nursery that reclaims dead young objects for free). On the nursery's cost, an earlier claim in this ledger was overstated and is corrected here. The 0.05 ms vs 2.62 ms worst-pause figure comes from -`benchmarks/wintercg/pause.sx`, where every allocation dies immediately -- +`benchmarks/wintertc/pause.sx`, where every allocation dies immediately -- the pattern that most flatters refcounting and most penalises a collector, which must still scavenge. Re-measured on `benchmarks/workload/ pause_survivors.js`, which keeps 2000 objects live while churning 2M, the diff --git a/spec/LANGUAGE.md b/spec/LANGUAGE.md index d7041b3..978579b 100644 --- a/spec/LANGUAGE.md +++ b/spec/LANGUAGE.md @@ -19,6 +19,14 @@ Safe object shapes reject property addition/deletion and incompatible writes. The compatibility transformer erases this qualifier; the native parser is responsible for attaching its runtime descriptor. +The declared type is what `safe` specializes on, so it is part of the +contract rather than documentation. `safe ... : i32` wraps at the 32-bit +boundary, which is the defined semantics for that type and not JavaScript's; +every other annotation, and an un-annotated `safe`, keeps exact JavaScript +arithmetic where 2^31-1 + 1 promotes to a double. A declared signature is +also what makes a call eligible to be inlined into its caller, so annotating +a small function changes what it costs. + Primitive FFI declarations use an explicit unsafe boundary: ```sx @@ -49,6 +57,17 @@ document has not written down. - A borrow cannot be returned, stored in a longer-lived value, or captured. - `unsafe` permits typed JS/native interop but never disables runtime alias locks. +Of those, the exclusive-borrow rule is the one enforced today: `&mut x` where +`x` is a binding the parser can resolve and that was not declared `let mut` is +a compile error, `SX2003`. It rules only on a bare identifier it can resolve in +the current function's lexical scope chain or its top-level lexicals; a +parameter, a captured outer binding, or a name it cannot resolve is left alone +rather than guessed at. The rest of this section is parsed and waiting on the +control-flow ownership pass; `spec/IMPLEMENTATION.md` is the record of which is +which. Note also that `&mut` is still erased at runtime, so it aliases through +JavaScript object identity: it mutates a struct in place, and cannot write back +to a caller's number. + `i32`, `f32`, `f64`, `bool`, and ordinary JavaScript values are copyable. Primitive-only interfaces define affine fixed-layout structs. A literal becomes such a struct only in an explicit annotation, typed argument, or typed return diff --git a/spec/NODE.md b/spec/NODE.md index c00704f..4e63478 100644 --- a/spec/NODE.md +++ b/spec/NODE.md @@ -31,6 +31,14 @@ in the order `import`, `module`, `default`, `require`, `node`, then falling back to `module`, then `main`. Circular `require` sees the same partially filled `exports` a cycle sees in Node, rather than recursing forever. +A `node:` specifier is registered as a module when the loader is asked for +it, not at startup: registering all forty-four up front cost a JSModuleDef +and an atom per export name on every launch, for a program that imports two +of them. `require` never went through that path at all -- it reads a static +table (`sxn_builtin_table` in `src/node.c`) and takes one property off the +global object. The seventeen builtins past the original twenty build their +objects on first use for the same reason. + ## `.node` addons `require("./thing.node")` and the `process.dlopen` it calls under the hood @@ -43,30 +51,56 @@ desktop-and-server capability, unlike `Sxn.ffi`. ## `node:` builtins -24 of the ~37 Node ships. What each one covers, briefly, and where it's -worth knowing the gap: +37 of the ~37 Node ships, counting base names rather than the `/promises`, +`/strict` and `/web` sub-paths (which also resolve, and bring the total to +44). It was 20 base names before; the seventeen added are described after the +table below. `require("module").builtinModules` is the whole list, read off +the same table `require` itself uses -- it used to be written out by hand in +JavaScript, and had fallen behind. What each one +covers, briefly, and where it's worth knowing the gap: | Module | Covers | |---|---| | `assert`, `assert/strict` | The standard assertion functions. | | `buffer` | See below — this one gets its own section. | | `crypto` | `Hash`, `Hmac` (standard construction over the digest primitive), `randomBytes`, `randomUUID`, `timingSafeEqual`. | -| `events` | `EventEmitter`, including the mixin pattern (`Object.assign(fn, EventEmitter.prototype)`) Express uses, where `_events` is created lazily on first `on()`/`emit()` rather than in a constructor that never runs. | -| `fs`, `fs/promises` | File I/O, sync and promise-based. | +| `events` | `EventEmitter`, including the mixin pattern (`Object.assign(fn, EventEmitter.prototype)`) Express uses, where `_events` is created lazily on first `on()`/`emit()` rather than in a constructor that never runs. `setMaxListeners`/`getMaxListeners` and `defaultMaxListeners` are real: crossing the limit raises Node's `MaxListenersExceededWarning` through `process.emitWarning`, once per emitter and event, which is the standard signal that handlers are being added and never removed. | +| `fs`, `fs/promises` | `readFile`/`writeFile` and their sync forms, `existsSync`, `stat`/`lstat` and their sync forms with a real `Stats`, and `createReadStream` (which reads the file, rather than windowing a file too large to hold). | | `http` | `createServer`, `IncomingMessage`, `ServerResponse`, `ClientRequest`, `STATUS_CODES`, `METHODS`. The request body defers behind `_read` rather than pushing eagerly, because a body-parser attaches its listener after the handler returns — push first and it gets nothing. | | `module` | The `Module` constructor (what `require('module').prototype` expects), `createRequire`, `builtinModules`, `isBuiltin`. | | `net` | `isIP`/`isIPv4`/`isIPv6`, including IPv6 zone-index stripping (`fe80::1%eth0`). `Socket`/`Server` are not implemented and throw. | -| `os`, `path`, `querystring`, `url`, `util` | The usual surface — `inspect`, `format`, `promisify`, `deepEqual`, POSIX/Win32 path handling, and so on. | +| `os` | Answered by libuv, not guessed: `hostname`, `cpus`, `totalmem`/`freemem`, `loadavg`, `uptime`, `networkInterfaces`, `availableParallelism`, `homedir`/`tmpdir`, `type`/`release`/`version`/`machine`, `userInfo`. | +| `path`, `querystring`, `url`, `util` | The usual surface — `inspect`, `format`, `promisify`, `deepEqual`, POSIX/Win32 path handling, and so on. | | `perf_hooks` | Enough for timing code that reads `performance.now`-equivalent values. | | `process` | `platform`, `arch`, `version`/`versions`, `stdout`/`stderr`/`stdin`, `hrtime`, `emitWarning`, `uptime`, `pid`, `env`, `argv`, `dlopen`. | | `stream`, `stream/promises` | `Readable`/`Writable`/`Duplex`/`Transform`/`PassThrough`, `pipeline`, `finished`. The module export is the `Stream` function itself (some packages `require('stream')` and call it as a constructor), and `Readable` supports real `pipe`/`unpipe` — the latter matters because `finalhandler` calls it on every response, piped or not. | | `string_decoder`, `timers`, `timers/promises`, `tty` | Small, focused shims. | | `zlib` | `gzipSync`/`gunzipSync`/`deflateSync`/`inflateSync` and the stream equivalents (`createGzip` etc.), over the zlib already linked in. No `promises` namespace — Node doesn't have one either. | -Not implemented: `child_process`, `cluster`, `dns`, `http2`, `https`, -`readline`, `stream/web`, `tls`, `v8`, `vm`, `worker_threads`, `inspector`, -`async_hooks`. `child_process` is the one that stops `next build` today — -see `spec/NATIVE.md`'s account of running Next.js's own compiler for the +### The seventeen added on top of the original 20 + +| Module | Covers | Where it stops | +|---|---|---| +| `child_process` | `spawn`, `exec`, `execFile`, `fork`'s siblings and every `Sync` form, over `uv_spawn` in `src/network.c`: arguments, `cwd`, `env`, stdin input, stdout and stderr, exit status and signal. | The child runs to completion on a loop of its own, so the asynchronous forms are the synchronous run plus the events Node would have emitted. Output arrives in one piece at the end rather than as it is produced, and `fork` throws — a child would need a second runtime. | +| `dns`, `dns/promises` | `lookup`, `resolve4`, `resolve6` through `uv_getaddrinfo`. | The system resolver is the only resolver: `resolveMx` and the other record types throw `ENOTIMP`, and `setServers` throws. Resolution blocks; the callback still arrives on a later tick. | +| `dgram` | Real UDP: `bind`, `send`, `message`, on a `uv_udp_t` on the main loop. | No multicast. | +| `https` | `request`/`get`, which is `node:http`'s client — the same native fetch, which speaks TLS. | No server: `Sxn.serve` does not terminate TLS. | +| `tls`, `http2` | Named, so a `require` resolves and a feature check answers. | Every entry point throws with the reason. TLS is client-side only, through fetch and `node:https`; the server speaks HTTP/1.1. | +| `stream/web` | The global Web Streams, under Node's names. | Nothing is reimplemented; the objects are identical (`require("stream/web").ReadableStream === globalThis.ReadableStream`). | +| `vm` | `runInThisContext`, `runInNewContext`, `Script`, `compileFunction`. | One realm: a "new context" is a function whose parameters are the sandbox's keys, not an isolated global. | +| `v8` | `getHeapStatistics` under Node's key names, `serialize`/`deserialize`. | The numbers come from QuickJS's allocator, not V8's. The serializer is JSON, so it carries plain data and rejects the rest. | +| `worker_threads`, `cluster` | The questions asked before a library decides whether it is the main one: `isMainThread`, `threadId`, `isPrimary`, `workers`. | One JS thread, one process. `Worker` and `fork` throw. | +| `readline`, `readline/promises` | Lines out of any readable stream, `question`, and `for await`. | No terminal editing: no history, completion, or cursor keys. | +| `async_hooks` | `AsyncLocalStorage` — `run`, `getStore`, `enterWith`, and a store that survives an `await` by riding the promise the callback returns. | There is no async context tracking underneath, so code that runs *while* that promise is pending sees the store too. `createHook` is inert. | +| `inspector` | `url()` answering "no session". | No debug protocol; `open` and `Session` throw. | +| `punycode`, `diagnostics_channel`, `console`, `constants` | RFC 3492 in full; named channels with subscribers; the global console; the flag numbers, taken from this platform's own headers rather than written down. | `diagnostics_channel`'s tracing helpers publish but do not track async context. | + +Everything that is not supported throws with the reason in the message, +rather than being absent — a feature check gets an answer instead of a +`MODULE_NOT_FOUND`. + +`child_process` was the module that stopped `next build`; see +`spec/NATIVE.md`'s account of running Next.js's own compiler for the full trace of what does and doesn't stand in the way. ## Buffer @@ -87,6 +121,12 @@ runtime fails the fixture: high surrogate half masks down to `=`. - `Buffer.byteLength`, `compare`, `equals`, `concat`, `toJSON` (Node's `{type:"Buffer",data:[...]}` shape). +- The numeric accessors — `readUInt32BE`, `writeFloatLE`, `readBigInt64LE` + and the rest of the forty, plus the variable-width `readUIntBE`/`writeIntLE` + family — `write`, `copy`, `swap16`/`swap32`/`swap64`, `Buffer.compare` and + `isEncoding`. +- `Buffer.from` copies when given a Buffer or a typed array, as Node does; + it shares only when given an ArrayBuffer. ## Encoding-name and Buffer performance @@ -96,3 +136,348 @@ site is recognized by pointer identity against the atom table rather than by hashing and comparing, and `Buffer.byteLength` computes the UTF-8 byte count directly rather than encoding the string to measure it. Both are covered in more depth, with numbers, in the README's benchmark section. + +## What is C and what is JavaScript + +This layer started as one JavaScript file and has been moving into C a piece +at a time. What has gone over is what C is actually better at: byte and +string work with no JavaScript state of its own. + +Native now: `path` in both halves, `querystring`, `net.isIP`, `os` in full +(from libuv), `fs`'s `stat`/`lstat` and the read primitives, `crypto`'s +digests, HMAC and `timingSafeEqual`, `zlib`'s deflate and inflate, Buffer's +encodings including latin1, Node's 7-bit `ascii` and utf16le in both +directions, lenient hex/base64 readers and numeric accessors, `util.format`, +`EventEmitter`'s `on`/`emit` fast path, and the structural comparison behind +`assert.deepStrictEqual`, `assert.deepEqual` and `util.isDeepStrictEqual`. + +The last three moves are worth reporting honestly, because they say where the +seam is. The lenient base64 reader and `util.format` are correct and match +Node exactly, but neither is much faster than the JavaScript it replaced -- +`format("a plain message")` went from 0.24 to 0.18 microseconds and the rest +is a wash. The deep comparison is slower: 4.58 microseconds per compare of a +small nested object against the JavaScript's 4.18, because every step of it +is a call back into the engine to read a property or compare two values, and +the interpreter does that for itself more cheaply than `JS_GetProperty` does +from outside. It is in C because this layer is being consolidated there and +it is the last piece of `node_compat.js` carrying real logic rather than +glue, and it is now checked against Node's own answers for 130 pairs, which +the JavaScript never was. Both facts belong in the same sentence. The work in +all three had already shrunk to the JavaScript-to-C boundary itself. That is +the shape of what is left everywhere else in this file. + +The move after them was the opposite kind: `Buffer#toString("latin1")` and +its utf16le and `ascii` siblings were a `String.fromCharCode` appended in a +loop, which builds a whole new string per byte. Filling the code units in C +and handing the engine one string took 4KB of latin1 from 395 microseconds to +0.5. Nothing here is a boundary crossing per byte, which is why it moved so +far when the deep comparison did not move at all. + +`require()` of a builtin moved for a third reason again: the specifier-to- +module table was an object literal rebuilt on every call, before the name was +even looked at. Static in C, a `require("node:path")` went from 2.30 +microseconds to 0.18. + +Inside `node:http`, the per-request header work moved too: lowercasing every +name and flattening it into `rawHeaders` was two JavaScript walks over the +same keys and is now one pass in C, 1.74 microseconds to 0.60 for a seven- +header request. That is 1.1 of the layer's 5.6 microseconds, taken from the +one part of it that is string work rather than object construction. + +`node:crypto` had one of these left inside it: `update(data, "hex")` parsed +the string with `parseInt` on a two-character `substr` per byte, and base64 +went through `atob`. Both now go through Buffer's native readers, which were +already there -- 4KB of hex input fell from 168 microseconds to 4.5 including +the digest itself. + +`res.end()` joins what was written into one body, and that walked the chunk +list three times -- once to ask whether any of it was binary, once to convert, +once to join. In C the three shapes every real response actually is are +answered directly: one string went from 0.41 microseconds to 0.02, two byte +chunks from 0.74 to 0.17. A mix of strings and bytes is handed back to the +JavaScript, because concatenating strings is the engine's own job and C would +have to re-encode them to do it. + +Object construction turned out to move as well, which the earlier note here +that C "would pay more at the boundary than it saves" got wrong for two +cases. `EventEmitter#once` allocated a closure that had to name itself in +order to remove itself; as a C function carrying the emitter, the name and +the listener, a once-and-emit went from 0.55 microseconds to 0.44. The socket +hung off every `node:http` request was eleven properties copied onto a fresh +emitter per request; the shape never varies, so the prototype is built once +and each request gets an object pointing at it -- 0.96 microseconds to 0.085, +another sixth of the layer's cost. The rule is not "objects stay in +JavaScript": it is that C wins wherever the work is repeated setup and loses +wherever it is a call back into the engine per step. + +The same rule took two more closures out of every request. `IncomingMessage` +built one to defer pushing the body until something reads, and another to +mark itself complete on `end`; both are now shared native functions reading +their state off the request. Building the two closures cost 0.084 +microseconds a request against 0.010 for the two fields that replaced them. + +`res.setHeader` and its three siblings lowercase the name on every call, and +that is a scan over a short string rather than a call back into the engine +per step: 0.17 microseconds a call became 0.058. + +`fs.statSync` had the same shape of waste: the native call built an object +with fifteen fields on it, and JavaScript then copied every one of them onto +a fresh `Stats` with a `for-in` loop and added four Dates. The native call +takes `Stats.prototype` now and fills the object once -- 3.30 microseconds a +stat became 2.75. + +The same three-loop join -- total the lengths, allocate, copy each part in -- +existed four times over: `Buffer.concat`, `node:crypto`'s digest input, +`node:zlib`'s stream flush and `res.end()`. There is one now, in C, one +memcpy per part: `Buffer.concat` of three 512-byte parts went from 0.77 +microseconds to 0.36. + +The two lenient readers finally lost their JavaScript halves. Node's hex and +base64 readers stop or skip rather than throwing, and they read a string a +byte at a time, so a code unit above 0xff is truncated -- which is why an +emoji ends a base64 string. The native readers used to hand such a string +back and let a JavaScript loop do it; they read the code units themselves +now, and the loops are gone. + +A second was tried and thrown away for a better reason than speed. A stream +pushing a `Uint8Array` wraps it in a Buffer over the same bytes, which costs +0.24 microseconds a chunk; giving the array Buffer's prototype in place costs +0.11. But the array belongs to whoever pushed it, and changing its prototype +changes what their own object is. The faster answer was the wrong one. + +One move was tried and thrown away, which is worth writing down because the +number is the only thing that settles it. A `Readable`'s constructor sets ten +own fields; done from C instead, the same ten stores cost 0.40 microseconds +against the interpreter's 0.34. `JS_SetPropertyStr` from outside is slower +than the interpreter's own store on a fresh object, so plain field +initialisation stays where it is. The socket above is not a +counter-example -- what made it fast was not setting the fields at all. + +`Writable#write` built a closure per chunk for the callback it must hand +`_write`, closing over a `backpressure` flag that nothing ever set. It is a C +function carrying the stream and the caller's callback now. This one bought +almost nothing -- 0.195 microseconds a write became 0.190 -- and the first +version of it, a shared no-op for writes with no callback, was faster at +0.130 and wrong: it swallowed the error a failing `_write` reports, which +has to reach the stream's 'error' listeners whether anyone passed a callback +or not. + +`StringDecoder` is native for utf-8 now -- it walks back at most three bytes +for a sequence that has not all arrived and keeps it for the next chunk, +where it used to run a `TextDecoder` with `{ stream: true }` per chunk: 0.330 +microseconds to 0.120. The other encodings keep the `TextDecoder`. Node's own +answer for a stranded byte is one replacement character for the character +that never arrived, not one per byte, which its own output settled. + +`node:util`'s `promisify`, `callbackify` and `inherits` were measured and +left alone: at 0.27, 0.14 and 0.55 microseconds they are already faster here +than in Node, which spends 1.37, 1.18 and 0.82 on the same three. + +`process.nextTick` copied `arguments` into an array and built a closure over +it every time; it carries up to three arguments in the C closure now and +keeps the rest in an array, 0.620 microseconds to 0.143. A magic value of -1 +for the array case is what found a sharp edge in the engine: `JS_NewCFunctionData` +stores its magic unsigned, so -1 came back as 65535 and the call read 65535 +arguments off a four-slot array. It is 4 now. + +`util.inspect` was the largest piece of real logic left, and the widest gap: +5.26 microseconds for a small object against Node's 1.56. It built an options +object per level, a mapped array per container and a joined string per level. +The C one walks the value into a single buffer and prints the same shapes: +2.26 microseconds, checked value by value against the JavaScript it replaced +before that was deleted. Two things it prints better -- an invalid date, which +used to throw out of `toISOString` at whoever tried to print it, and an +error, which now carries the `Error: message` line this engine's `stack` +leaves off. + +`util.promisify` is native too. It spread the arguments, built a Promise +around an executor closure and then a callback closure inside that, per call: +0.62 microseconds to 0.29, against Node's 0.08. Two behaviours changed to +match Node rather than what was here: a callback reporting more than one +value resolves with the first, not with an array of them, and a function that +throws synchronously produces a rejection rather than throwing out of the +call. + +`util.callbackify` built two closures per call, one for each half of the +promise; they are C functions carrying the callback now, 1.32 microseconds to +0.82 against Node's 0.38. It also picked up Node's handling of a falsy +rejection along the way: an `Error` saying so, with the original on `reason`, +where this used to invent a bare "rejected". + +The stream async iterator was measured and left alone: 0.38 microseconds an +item against Node's 0.12, and what it spends is a promise and a result object +per item, which is the iteration protocol rather than anything C could skip. + +`Buffer.from` split in two on the evidence. Copying a view is C now, 0.285 +microseconds to 0.210, because it was running the `Uint8Array` subclass +constructor per call. Copying a plain array is not: reading its elements one +at a time from C measured 1.185 microseconds against the engine's own 0.375, +which fills the array without leaving the interpreter. The same function, two +opposite answers, and only the measurement separates them. + +`pipe` moved for completeness rather than for speed: its four closures, one +per event, are four C functions sharing the source and the destination, and +that is worth 1.95 microseconds to 1.83. It was already five times quicker +than Node's, which does a great deal more bookkeeping. Kept because it is +four fewer allocations per pipe and costs nothing, not because the number +means much. + +A third sweep, over what a server actually touches, found two corruptions +rather than costs. `fs/promises.readFile` read the file as text and encoded +it back to bytes, so every byte that is not valid UTF-8 came back as the +replacement character -- a binary file was destroyed by reading it. It uses +the same native read the synchronous side does now, which is also three times +faster. `fs.writeFileSync` had the mirror of it: everything went through +`JS_ToCStringLen`, so a Buffer was written as its decimal digits. Bytes are +written as bytes now. + +The second sweep, over `node:util`, `node:string_decoder` and `process`, +found something worse than any of the migrations: `process.cwd()` cost 7 +microseconds against Node's 0.01. It was calling `getcwd()` every time, and +that walks the directory back to the root. Every relative path a package +resolves goes through it. It is cached now, and `process.chdir()` -- which +this runtime did not have at all -- is what clears the cache: 0.065 +microseconds. + +The sweep also turned up a bug rather than a cost. A `Readable` took chunks +off its queue with `shift()`, which copies the whole queue down by one every +time, so draining 20000 buffered chunks took 44 milliseconds against 1 for +2000 -- quadratic in the queue's length. Chunks leave through a cursor now: +the same 20000 take 3 milliseconds. This one is not a migration at all, and +no amount of C would have found it. + +A sweep of what is left, measured rather than guessed, so the next person +does not have to re-derive it: + +| Piece | Cost here | Where it goes | +| --- | --- | --- | +| `path.extname` | 0.060 us | already C | +| `Writable#write` | 0.130 us | C no-op callback; the rest is `_write` | +| `querystring.stringify` | 0.155 us | already C | +| `path.join` | 0.225 us | already C | +| `querystring.parse` | 0.235 us | already C | +| `Readable#push` + emit | 0.290 us | the emit is C; the wrap is not (see below) | +| `PassThrough#write` | 0.600 us | two emitter hops, both already C | +| `url.fileURLToPath` | 0.130 us | C, was 0.475 | +| `module.isBuiltin` | 0.045 us | C, was 0.415 | +| `util.inspect` of an object | 2.26 us | C, was 5.26; Node is 1.56 | +| a promisified call | 0.29 us | C, was 0.62; Node is 0.08 | +| `readable.pipe` | 1.83 us | C, was 1.95; Node is 7.21 | +| `Buffer.from` a view | 0.21 us | C, was 0.285; Node is 0.035 | +| `util.inherits` | 0.060 us | C, was 0.250; Node is 0.150 | +| a callbackified call | 0.82 us | C, was 1.32; Node is 0.38 | +| iterating a buffered stream | 0.38 us | JS: a promise and an object per item, which is the protocol | +| `Buffer.from` an array | 0.37 us | JS: from C it measured 1.185 | +| `process.nextTick` | 0.143 us | C, was 0.620 | +| `res.getHeaders` | 0.110 us | C, was 0.173 | +| `res.getHeaderNames` | 0.110 us | C, was 0.157 | +| `StringDecoder#write` | 0.120 us | C, was 0.330 | +| `res.writeHead` | 0.330 us | JS: it is setHeader in a loop, and that is C | +| `new Writable` | 0.360 us | JS: field stores, measured slower in C | +| `new Readable` | 0.500 us | JS: same | +| `readable.on("data")` | 1.16 us | JS: 0.5 of it is the deferred drain, which is the semantics | +| `new URL` | 0.885 us | the engine's own | +| `url.pathToFileURL` | 0.92 us | C text, was 1.30; `new URL` is 0.885 of what is left | +| `createRequire` | 1.00 us | already C | + +And the last sweep, over what the section table above still calls +JavaScript, so that nothing is left merely assumed: + +| Piece | Cost here | Why it stays | +| --- | --- | --- | +| `util.types.isDate` | 0.050 us | one `instanceof` | +| `net.isIP`, `isIPv4` | 0.050 us | the wrapper around a native call | +| `url.format` | 0.040 us | `String(url)` | +| `readable.unpipe` | 0.700 us | Node's is 0.300; it is `off` three times, and `off` is C | +| `url.parse` | 0.900 us | the engine's `new URL` in a try | +| `createRequire` | 0.900 us | already native underneath | +| `zlib.gzipSync` of 240 bytes | 2.50 us | zlib itself; Node is 3.40 | +| iterating a buffered stream | 0.380 us | a promise and a result object per item, which is the protocol | +| `new Readable` / `new Writable` | 0.500 / 0.360 us | field stores, measured slower from C | +| `res.writeHead` | 0.330 us | `setHeader` in a loop, and that is C | +| `Buffer.from` an array | 0.375 us | from C it measured 1.185 | + +Two of those were then attacked directly, since the user asked. + +**zlib.** Every call ran `deflateInit2` and `deflateEnd`, and those allocate +and free the window and the hash tables -- a quarter of a megabyte at these +settings -- around a compression of 240 bytes. zlib's own answer is +`deflateReset`, which keeps the state and the settings, so one stream per +direction is kept and reset instead; a change of window bits or level throws +it away and builds a fresh one. `gzipSync` of 240 bytes went from 3.45 to +2.50 microseconds, `deflateSync` from 2.85 to 2.25, `gunzipSync` from 3.35 to +2.90. Every one of those is now faster than Node's, which spends 3.40, 3.20 +and 4.05 on the same calls. Swapping zlib for zlib-ng or libdeflate was +considered and rejected: both are a new dependency, libdeflate cannot stream +at all, and this code is already ahead of Node without either. + +**unpipe.** Removing a listener rebuilt the whole listener array, allocating +a new one and copying every entry, on every removal. `off` now finds the +first match and closes the list up in place. The catch is that `emit` walks +that same array, and Node emits to the set of listeners that existed when it +started, so `emit` counts its own depth and `off` still takes the copying +path while an emit is running. Unpiping 20000 sources from one destination +went from 368 to 58 microseconds a call, against Node's 168; removing a +listener from a list of 200 went from 4.87 to 1.24 microseconds. An ordinary +`unpipe`, where the destination has one pipe on it, is unchanged at 0.70. + +Nothing in that list is a JavaScript loop any more. What remains in the file +around them is dispatch: argument shuffling, a check, and a call into +something that is already native. + +### Section by section, what is left and why + +Every section of `src/node_compat.js`, with what is native under it and what +the JavaScript around it still does. Nothing here is a loop over bytes or +characters any more; what is left is dispatch, class shapes and event +plumbing, and the cost of each of those was measured before it was left +alone. + +| Section | Lines | What is native | What the JavaScript still does | +| --- | --- | --- | --- | +| `events` | 74 | `on`, `off`, `emit`, `once`, `listeners`, `listenerCount`, `removeAllListeners`, the max-listener count check | the class shape, `setMaxListeners`/`getMaxListeners`, and the two async helpers `once(emitter)` and `on(emitter)`, which are promise plumbing | +| `buffer` | 146 | every encoding both ways, the lenient readers, `concat`, `compare`, copying a view, the numeric accessors | `Buffer.from`'s dispatch on argument type, and `toString`'s on encoding name | +| `path` | 56 | all of it, both posix and win32 | the two tables and the platform choice between them | +| `process` | 108 | `env`, `cwd`, `chdir`, `nextTick`, `exit`, `pid`, `platform`, `arch`, signal watching | `argv`, the stdio objects, `emitWarning`, `uptime` | +| `fs` | 89 | reads, writes, `stat`, `exists` | the encoding branch, `Stats`' predicates, `createReadStream`'s wrapper | +| `stream` | 372 | the chunk queue's cursor, `write`'s callback, `pipe`, the byte joining | the five classes, `pipe`, the async iterator, the Web Streams bridges -- listener bookkeeping, measured slower from C | +| `http` | 201 | header lowercasing, `rawHeaders`, the socket, the body join, the four header methods, the deferred body | `IncomingMessage` and `ServerResponse` themselves, and the server's promise contract | +| `net` | 20 | `isIP` | the two wrappers around it, and the honest refusals for real sockets | +| `crypto` | 94 | digests, HMAC, `timingSafeEqual`, random bytes, every input encoding | `Hash` and `Hmac`'s two-line classes, and the digest encoding branch | +| `zlib` | 90 | deflate and inflate | the six wrappers, the constants, the Transform streams | +| small builtins | 95 | `StringDecoder`'s utf-8 path, the builtin table behind `require` | `tty`, `timers`, `perf_hooks`, `Module`'s shape | +| `util` | 106 | `format`, `inspect`, `promisify`, `callbackify`, `inherits` | `types`, which is fourteen one-line predicates | +| `assert` | 31 | the structural comparison | `AssertionError` and the twelve one-line entry points | +| `os` | 37 | all of it, from libuv | the object it hangs on | +| `querystring` | 17 | all four functions | the object it hangs on | +| `url` | 16 | `fileURLToPath`, `pathToFileURL`'s text | `format` and `parse`, which are the engine's `URL` | +| the seventeen added modules | 746 | spawning a process, resolving a name, the UDP socket, fs's flag numbers | the module shapes around those four calls, and the modules that are answers rather than work -- all of it built on first use, not at startup | + + +Still JavaScript, with the reason measured rather than asserted: + +- **`stream` and `http`.** A `node:http` request costs 13.4 us here against + `Sxn.serve`'s 7.8 for the same reply, so the layer is 5.6 us of JavaScript + -- worth attacking, if C could take it. It cannot: 0.9 us of that is + constructing the Readable and the Writable, which are the API, not an + implementation detail a handler cannot see; the rest is property writes and + listener bookkeeping on those same objects, which C would perform through + `JS_SetProperty` at more cost than the interpreter's own store. The gap is + visible in the constructors themselves -- `new Readable` is 0.50 us here + and 0.036 in Node -- and that is the no-JIT tradeoff this runtime has + chosen, not something moving the file to C would change. +- **`util.inspect` and `assert.deepStrictEqual`** walk arbitrary JavaScript + values. Every step would be a `JS_*` call; the C would be longer and no + faster. +- **Buffer's lenient hex and base64 readers** are defined over UTF-16 code + units — Node reads a string one code unit at a time and masks it, which is + why an emoji ends a base64 string. A C function receives UTF-8 and cannot + see that. +- **Thin wrappers** — `zlib`'s callback and promise forms, `fs`'s encoding + branch, `process`, `os`'s method objects — are three lines each around a + native call, and moving them would add C without removing work. +- **The modules that answer rather than compute** — `worker_threads`, + `cluster`, `tls`, `http2`, `inspector`, `vm`, `v8` — are constants and + refusals. There is no work in them to move. +- **`punycode`** runs once per hostname at most, on a string short enough + that the arithmetic is not the cost. diff --git a/spec/PERFORMANCE.md b/spec/PERFORMANCE.md index 52c61eb..b524e7f 100644 --- a/spec/PERFORMANCE.md +++ b/spec/PERFORMANCE.md @@ -204,19 +204,248 @@ was missing here entirely and is now native: it walks the string and counts, surrogate pairs and the three-byte replacement for unpaired surrogates included, without encoding it. -Cumulatively, on the Mac: Buffer 102->19.2 ms, TextEncoder 76->4.6 ms, -EventEmitter 37->6.6 ms, cold start 10.7->8.3 ms, and the pause benchmark's -total 1.1 s->143.9 ms, with +Cumulatively, on the Mac: Buffer 102->19.4 ms, TextEncoder 76->4.7 ms, +EventEmitter 37->6.7 ms, cold start 10.7->7.5 ms, and the pause benchmark's +total 1.1 s->146.6 ms, with zero GC cycles during the loops throughout. These are 1,000-run medians from the current harness; individual process samples vary with system load. +## JSON + +The harness's `json` row parses a 1MB API payload and writes it back out, +forty times. It is the one row named after a workload rather than an API, +because it is what an HTTP service spends its time on: a request body in, +a response body out. + +Three changes, all inside the tokenizer and the serializer rather than the +value model: + +- **Strings are scanned a word at a time.** Both directions used to walk a + string one character per iteration -- four comparisons per byte on the way + in, a `string_getc`/`string_buffer_putc` pair per character on the way + out. Both now test eight bytes at once for the only bytes that matter (a + quote, a backslash, a control character, and on the way in anything + non-ASCII), using the standard `(x - 0x01..) & ~x` zero-byte trick, and + copy the run between them in one `memcpy`. Scanning was the largest single + cost in each direction. +- **An escape-free string is allocated once.** The parser accumulated every + string into a `StringBuffer` that starts at 48 characters and is then + grown, copied and trimmed. A string with no escape and nothing above ASCII + -- most strings in most JSON -- is now measured by the scan above and + allocated at its final size. +- **Quoting writes into the buffer the caller already has.** `stringify` + allocated a quoted copy of every key and every string value, copied it into + its output, and freed it. It writes through now. + +- **A parsed string is not built until its use is known.** An escape-free + ASCII string is handed on as a slice of the input, so a property name goes + straight to an atom -- a hash of bytes already in memory -- instead of + allocating a string, interning it and freeing it again. Only a value is + allocated. +- **Getting at the input costs a scan, so the scan is a word wide.** + `JS_ToCString` already returns an ASCII 8-bit string's own bytes without + copying, but it decided that by counting non-ASCII bytes one at a time + across the whole string; it now clears eight at a time and only counts from + the first non-ASCII byte. Its wide-string transcode -- what an input pays + when one accent makes the whole string 16-bit -- copies four ASCII code + points per iteration instead of one. Both help every `JS_ToCString` caller + in the runtime, not only JSON. + +And a plain integer is converted by the tokenizer instead of `strtod`, which +is locale-aware -- it takes a lock to find the decimal separator -- and +rescans the digits. + +Then the object model, which the profile pointed at once the scanning was +cheap. Writing: + +- **An object's keys are read as atoms, not as strings.** `stringify` built + a JavaScript array of key strings per object and then looked each property + back up by string, which hashes it into an atom again. An ordinary object + with no replacer list now walks its own atoms, reads by atom, and takes the + key string from the atom rather than building one. When its keys are all + ordinary strings they come straight out of its shape into a stack array -- + no allocation per object at all -- and, while the shape has not changed, + each value is read from the slot recorded then rather than looked up. +- **Integers, booleans and null go in as bytes.** Each used to be handed to + `JS_ToString`, which allocates a string to copy in and free. A JSON + document is mostly those three. +- **`toJSON` is looked for once per shape.** Every object was searched for + the method, walking its prototype chain to find nothing. The last shape + with none is remembered against the runtime's property-location generation + -- the stamp the inline caches already keep. The memo is only taken when + nothing on the chain is exotic and nothing on it has a `toJSON` property at + all: a Proxy answers from its own trap and they all share one shape, and a + shape records which properties exist, not what they hold. +- **The circular-reference check left the JavaScript heap.** The path from + the root was a JavaScript array, so every object cost an `Array#includes`, + a push and a pop through the generic property machinery. It is a small + array of object pointers now. +- **Nothing is written for a separator that is empty**, which is both of them + whenever `JSON.stringify` is called without a gap. + +Reading: + +- **A property goes straight into the object being built.** The parser owns + the object it is filling and nothing else can see it, so a key that is not + already there is added directly instead of going through the descriptor + machinery. A repeated key -- legal, last one wins -- is the only case that + finds an existing slot. +- **An array element is appended, not defined.** The array is the parser's + own fresh fast array; appending to it skips the indexed-property path, + which has to assume the target could be anything. +- **The last few property names are remembered.** Every object in a document + of records repeats the same keys, and each one was hashed into the atom + table again. + +On the Mac the round trip went 165.4 -> 47.4 ms against Node's 28.5 and +Bun's 23.1. Per operation, against Node: + +| | this runtime | Node | +|---|---|---| +| parse a 1MB document | 2.29 -> 1.22 ms | 1.05 | +| write it | 7.01 -> 1.23 ms | 0.60 | +| parse 30000 small objects | 8.56 -> 4.44 ms | 2.36 | +| write them | 10.6 -> 3.69 ms | 0.95 | +| parse one long string | 0.94 -> 0.19 ms | 0.32 | +| write one long string | 2.52 -> 0.08 ms | 0.13 | + +Both string rows are now ahead of Node; the rest is within 1.2x on parsing a +document of mixed content and about 2x on writing one. + +Numbers were the other half, and are no longer. Turning a double into its +shortest round-tripping decimal was an exact big-integer search; `dtoa.c` now +takes Grisu3 (Loitsch, PLDI 2010) first, which computes the digits with +64-bit arithmetic and a table of cached powers of ten and then *proves* its +result is the unique shortest form, declining when it cannot. Every decline, +and every other radix, format and exponent mode, still runs the exact +algorithm. Stringifying 120000 random doubles: 22.8 -> 13.9 ms per pass. It +is checked by a differential test against Node over millions of values -- +random bit patterns, every integer from -1e6 to 1e6, powers of ten, +subnormals, the signed zeroes, infinities, NaN, and ULP windows around 1e21 +and 2^53 -- with no divergence. + +What is left is writing object-heavy documents, still around 4x Node and +spread across the remaining per-property work with no peak worth naming. + +Two of these went in wrong the first time and were caught by review before +they shipped: the `toJSON` memo carried one Proxy's answer to the next, and +re-checking enumerability per property let a getter change what was written +after the key list had been taken. `tests/fixtures/json_edges.mjs` covers +both, and `json_fuzz.mjs` walks 4000 seeded-random documents through parse +and stringify and hashes every string produced -- the expected hash is +Node's, so a byte of divergence anywhere fails the build. + +None of it is the parser's structure, which is why a faster external parser +is not the answer. A DOM parser would replace the part that is now cheap and +still leave every JavaScript object to be built one property at a time, with +a second representation to copy out of on the way. + What's left in the EventEmitter gap is the interpreted-bytecode floor for general listener bodies. The benchmark's numeric accumulator takes a native fast path and now a fused call site as well, but arbitrary listeners still require an interpreter frame. A JIT is the usual way to remove that frame, -and it's ruled out here; closing the gap some other way is open, and hasn't -been attempted yet. +and it's ruled out here; closing the gap some other way is open, and the +declared-signature inlining below is the first thing that has removed one. + +## Startup + +The Node layer used to register all forty-four `node:` modules at startup -- +a `JSModuleDef` and an atom per export name each -- and to construct every +module object, whatever the program went on to import. Both now happen when +something asks: the loader registers a module on the specifier it was handed, +and the seventeen builtins past the original twenty build their objects on +first use. Cold start on the Mac, minimum of 150 interleaved launches: +7.15 -> 6.97 ms. + +What is left above the 6.83 ms this measured before the Minimum Common API +work is `src/bootstrap.js`: `URLPattern`, the compression streams, the stream +controller classes and the three event-handler properties are built eagerly, +because a page-shaped global has to be there before the program's first line +runs. Deferring the node_compat half was worth about 0.2 ms; deferring this +half would mean a getter per global, and the globals are the surface. + +Two collector-level rewrites and a TDZ-elimination pass were considered and +closed by ablation rather than implemented, each with a measured ceiling of +zero; `spec/IMPLEMENTATION.md` records the method and the numbers. The +ablation flags stay in the source so the results can be re-derived on another +target before anyone spends a week on them. + +## The declared types buy something, and it is the call frame + +Every annotation used to be stripped, so `safe` was the only fact reaching +codegen and it did not carry which type had been written. Recording the type +instead let the same specialization be aimed properly, and let a second one +exist at all. Measured on the Mac, minimum of 9 runs over 5M iterations, +reported per loop iteration: + +| | sxn | Node 25.2 | Bun 1.2.17 | +|---|---|---|---| +| empty loop | 4.0 ns | 0.27 | 0.23 | +| interpreted call, 0 args | 14.4 ns | -- | -- | +| interpreted call, 2 args | 16.3 ns | 0.37 | 0.22 | +| field read + write | 11.9 ns | 0.27 | 0.22 | +| f64 accumulate | 5.9 ns | 0.53 | 0.54 | + +Read that the way this document reads every JIT row: Node and Bun delete these +loops outright, so no interpreter change reaches 0.3 ns. What the table +locates is where this runtime's own time goes. A call costs about 10.4 ns +before an argument is passed and 1.7 ns per argument after; a field access is +2.6 ns; arithmetic is already within 2 ns of the dispatch floor. Typed and +untyped field access measured 11.76 and 11.67 ns, the same number, which is +what "the annotations bought nothing" meant concretely. + +Hand-inlining a small typed function gave the exact upper bound for removing +the frame -- 17.1 ns to 5.4 for a two-argument `i32` add -- and unlike the +ablations below, that bound was not zero. A call whose callee is a local +written once by an `fclosure`, never captured, with every parameter and the +return declared scalar, and whose body loads each parameter once in order and +then runs only operand-free arithmetic, is now spliced into its caller at the +pass-2 peephole. `benchmarks/engine/call_inline_probe.sx`: + +| | ns/op | +|---|---| +| empty loop | 4.07 | +| untyped call | 16.34 | +| typed call, inlined | 5.31 | +| hand-inlined ceiling | 5.38 | + +16.34 to 5.31 ns, 3.1x, landing on the ceiling. Against Node's 0.37 ns that is +a 44x gap narrowed to 14x, and it should be quoted that way. It needs no +opcode, which matters because the opcode space is full -- the template-literal +`concat` op above took the last slot. Plain JavaScript never qualifies: the +gate is the declared signature, so a `.js` file is untouched. + +Two limits are worth stating rather than discovering. The splice needs the +body to load every parameter once in declaration order, so anything reusing a +parameter still pays for its frame. And an inlined callee no longer appears in +a stack trace if its arithmetic throws, which is the tradeoff every inlining +compiler makes. + +## An idle process never collected + +Not a speed result but a memory one, and the counterpart to the zero ceilings +below: the collector's cadence was worth far more than its mechanism. + +Nothing in `src/` called `JS_RunGC`. Collection happened only inside +`js_trigger_gc`, when an allocation crossed a threshold that is then set to +1.5x whatever was live. A burst of 200,000 cyclic objects, held while built +and then dropped, left 229 MB that 96 MB of subsequent churn did not collect +once -- the collection at the peak had raised the bar above what leaked, and +the bigger the garbage the higher the bar. + +The event loop now sweeps when it has been blocked waiting, is holding more +than 32 MB, and has not swept in half a second, and resets the threshold +afterwards. A burst-then-idle daemon reclaims 171 MB across one loop turn with +nothing calling anything; `Sxn.gc()` is the explicit ask. A server under +sustained load takes no sweep at all -- 4,000 requests, zero collections, the +same wall time with the feature on and off -- because `uv_run` never blocks +long enough. The rows above are unmoved, worst pause included. + +`benchmarks/engine/gc_idle_probe.sx` re-derives it, and reports RSS beside the +allocator's own accounting because the two diverge: after a sweep took tracked +bytes from 164 MB to 0.7, RSS moved only 199.8 to 196.8 MB. The system +allocator kept the pages. Assert on `mallocSize`; read `rss`. Two collector-level rewrites and a TDZ-elimination pass were considered and closed by ablation rather than implemented, each with a measured ceiling of diff --git a/spec/RUNTIME.md b/spec/RUNTIME.md index fb74c14..f4af3b5 100644 --- a/spec/RUNTIME.md +++ b/spec/RUNTIME.md @@ -1,8 +1,8 @@ # The runtime surface -This is what `sxn` gives you independent of Node compatibility: the WinterCG -web APIs (45 of 55 names in the common surface), the `Sxn` host namespace, and -the engine capabilities that go with it. `spec/NODE.md` is the other half — +This is what `sxn` gives you independent of Node compatibility: the WinterTC +web APIs (every name in the Minimum Common API except WebAssembly's), the +`Sxn` host namespace, and the engine capabilities that go with it. `spec/NODE.md` is the other half — what runs because it imitates Node. The split matters because only this half travels when the engine is embedded elsewhere (`spec/NATIVE.md` explains why for the native-code case @@ -17,10 +17,13 @@ same surface those runtimes implement. `sxn file.sx` runs an SxfeScript file — ordinary JavaScript plus explicit mutation, affine values, and borrow sigils, parsed natively with no separate -transform step (`spec/LANGUAGE.md`). `sxn file.ts` strips TypeScript types and -runs the result, also natively, with no build step. `sxn file.js` / `.mjs` / -`.cjs` run plain JavaScript. All four import each other freely: a `.sx` -module can `import` a `.ts` module and vice versa. +transform step (`spec/LANGUAGE.md`). `sxn file.ts` parses TypeScript types +natively and runs the result, also with no build step; no code is emitted for +an annotation, but a declared scalar type is recorded rather than discarded +and spent on the bytecode that comes out (`spec/LANGUAGE.md`, +`spec/PERFORMANCE.md`). `sxn file.js` / `.mjs` / `.cjs` run plain JavaScript. +All four import each other freely: a `.sx` module can `import` a `.ts` module +and vice versa. Module-or-script is decided the way Node decides it — see spec/NODE.md — with one exception: `.sx` and `.ts` are always modules, because type stripping is @@ -63,18 +66,49 @@ the shape the native layer speaks and `node:http` is built on directly. Response headers are emitted in declaration order; an array value repeats the header (the multi-`Set-Cookie` case), and `Content-Length`/`Connection` are filtered since those describe the framing rather than the payload the handler -wrote. The returned handle reports `port`, `url`, and a `stop()` that lets a -process serve and then do something else, rather than block forever the way a -bare listener would. +wrote. `options` takes `port`, `hostname` (or `host`, Node's name for it; loopback +by default, `"0.0.0.0"` to accept from the network, an IPv6 literal or +`"localhost"` also work), and `reusePort`. With `reusePort: true` several +processes bind the same port and the kernel spreads connections across them, +which is how one program uses more than one core here -- there are no threads +and no cluster module. The kernels that distribute this way are Linux, the +BSDs, Solaris and AIX; on macOS the call fails with a message saying so +rather than quietly giving the last process every connection. + +The returned handle reports `port`, `hostname`, `url`, and a `stop()` that +lets a process serve and then do something else, rather than block forever +the way a bare listener would; `stop()` closes the connections still open as +well as the listener. Connections are kept alive by default, as HTTP/1.1 requires, +and a request pipelined behind another is answered without waiting for a +further read. A request larger than 64MB is refused rather than buffered. ## Web Streams -`ReadableStream`, `WritableStream`, `TransformStream`, and both queuing -strategies, plus `TextEncoderStream`/`TextDecoderStream` built on them. A -fetch response body is a real `ReadableStream`, not a stand-in, so -`pipeThrough`, `pipeTo`, and `for await` all work on one. Not yet -implemented: `CompressionStream`/`DecompressionStream` (`node:zlib` covers -the same ground synchronously — see spec/NODE.md) and the BYOB reader. +`ReadableStream`, `WritableStream`, `TransformStream`, both queuing +strategies, and each of the controller and reader classes the spec names, so +`instanceof` answers the way it does elsewhere. `TextEncoderStream`/ +`TextDecoderStream` are built on them. A fetch response body is a real +`ReadableStream`, not a stand-in, so `pipeThrough`, `pipeTo`, and `for await` +all work on one. + +A BYOB reader (`getReader({ mode: "byob" })`) fills the view you hand it and +keeps whatever did not fit for the next read. What it does not do is let the +*source* write into your buffer: `byobRequest` is always null, so a source +written to only ever fill a `byobRequest` finds nothing to fill. Reading is +still a copy, one chunk at a time; what BYOB buys here is the calling shape, +not zero-copy. + +`CompressionStream`/`DecompressionStream` handle `gzip`, `deflate` and +`deflate-raw`, over the same zlib `node:zlib` uses, with one stream kept per +object so chunks compress as a single stream rather than one per chunk. + +## URLPattern + +`new URLPattern({ pathname: "/books/:id" })`, `test`, and `exec` with named +groups. Each URL component is compiled separately: `:name` captures up to the +component's own separator (`/` in a path, `.` in a hostname), `(...)` is a +regular expression written in place, `*` is anything, and `{...}?` makes what +it wraps optional. A component you leave out matches anything. ## Crypto @@ -91,10 +125,27 @@ of the same digests; see spec/NODE.md. `clearX` counterparts, `performance.now` (bound directly to its C primitive, not wrapped — see the README benchmarks for why that matters). -Not implemented: `URLPattern`, `BroadcastChannel`, `Worker`, `WebSocket` as an -*outbound client* (the server side — upgrading an incoming connection to a -WebSocket from a `Sxn.serve` handler — works), `ErrorEvent`, -`PromiseRejectionEvent`, and `Intl`. +`console.log`/`info`/`debug` write to stdout and `console.error`/`warn` to +stderr, which is also what `process.stderr` is built on. + +`ErrorEvent` and `PromiseRejectionEvent` exist, and so do the three handlers +that carry them. + +## Errors that reach the top + +The global object is an event target: `addEventListener("error", ...)` and +`onerror` both see an exception nothing caught, and either can keep it from +being printed — a listener by calling `preventDefault()`, `onerror` by +returning true. `reportError(e)` reports one without throwing it. + +A promise rejection nothing handled is reported once every job that could +still have handled it has run, as an `unhandledrejection` event; if something +handles it later, `rejectionhandled` follows. With no handler registered, +nothing changes: an unhandled rejection is as quiet as it was before. + +Not implemented: `BroadcastChannel`, `Worker`, `WebSocket` as an *outbound +client* (the server side — upgrading an incoming connection to a WebSocket +from a `Sxn.serve` handler — works), and `Intl`. ## `Sxn.ffi` — calling a C function @@ -113,8 +164,28 @@ that belongs to the engine rather than to Node compatibility. `Sxn.file(path)` and `Sxn.write(path, data)` for file I/O in the Bun-style idiom; `Sxn.memoryUsage()`; `Sxn.version`. +`Sxn.memoryUsage()` reports the engine allocator's accounting -- `mallocSize`, +`objects`, `gcCount` and the GC timings -- plus `rss`, which is what the +operating system says the process holds. The two move independently: the +system allocator may keep freed pages rather than return them, so `rss` can +stay high after a collection that reclaimed everything. Assert on +`mallocSize`; read `rss`. + +`Sxn.gc()` collects reference cycles and returns the tracked size left behind. +Refcounting frees everything else the moment its last reference goes, but a +cycle -- an object that points at itself, a closure the object it captures +also holds -- needs the collector, and the collector otherwise runs only when +an allocation crosses a threshold. A process that stops allocating stops +collecting, which is why a server is also swept while its event loop is idle; +see `spec/CLI.md` for `--no-idle-gc` and `--idle-gc-floor`. + ## What's deliberately not here +WebAssembly. It is the one part of the Minimum Common API this runtime does +not have, and it is not a small gap to close: QuickJS has no WebAssembly +engine, so `WebAssembly` is undefined rather than a stub that throws, which +lets feature detection do the right thing. + Anything that only makes sense with a machine-code tier — a JIT, or `process.dlopen`/`.node` addons — lives in the Node-compatibility layer instead, not here, precisely so a build of this runtime that drops that layer diff --git a/src/bootstrap.js b/src/bootstrap.js index cfdbdcb..299b10d 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -1,4 +1,4 @@ -/* WinterCG-ish globals layered on top of the native bindings installed by +/* WinterTC-ish globals layered on top of the native bindings installed by sxn_install_network (the __sxn* functions below). Pure parsing/spec logic lives here in JS; anything needing tight C integration (the streaming fetch body, random bytes, digests, the monotonic clock) is a thin native @@ -572,7 +572,7 @@ AbortController.prototype.abort = function (reason) { this.signal._doAbort(reason); }; globalThis.AbortController = AbortController; - // ---------------- Blob (WinterCG) ---------------- + // ---------------- Blob (WinterTC) ---------------- // Bytes are concatenated eagerly at construction time into a single // Uint8Array (this._bytes) -- a deliberately minimal, honestly-scoped // in-memory backing store rather than a lazy/streamed one, matching the @@ -681,6 +681,10 @@ if (init.headers !== undefined) this.headers = new Headers(init.headers); if (init.body !== undefined) this._body = init.body; if (init.signal !== undefined) this.signal = init.signal; + // Fetch's redirect mode travels with the request, so `fetch(request)` + // honours what the Request was built with. + this.redirect = init.redirect !== undefined ? init.redirect + : (input instanceof Request ? input.redirect : "follow"); } Object.defineProperty(Request.prototype, "body", { get: function () { return null; } }); Request.prototype.clone = function () { return new Request(this); }; @@ -704,7 +708,19 @@ if (typeof b === "string") { this._bodyUsed = true; return b; } return new TextDecoder().decode(this._readBytes()); }; - Request.prototype.json = async function () { return JSON.parse(await this.text()); }; + Request.prototype.json = async function () { + // Straight from the bytes when that is what arrived: decoding a megabyte + // into a string and parsing the string costs two passes and a copy that + // the parser does not need. + var b = this._body; + if (b instanceof Uint8Array && typeof __sxnParseJSONBytes === "function") { + this._bodyUsed = true; + // JS_GetUint8Array hands back the view's own start and length, so the + // offset is already accounted for. + return __sxnParseJSONBytes(b); + } + return JSON.parse(await this.text()); + }; Request.prototype.arrayBuffer = async function () { return this._readBytes().slice().buffer; }; Request.prototype.blob = async function () { return new Blob([this._readBytes()], { type: this.headers.get("content-type") || "" }); @@ -839,12 +855,21 @@ return Promise.reject(signal.reason !== undefined ? signal.reason : domError("AbortError", "The operation was aborted.")); } var body = request._body !== undefined && request._body !== null ? String(request._body) : undefined; - var raw = __sxnFetchRaw(request.url, request.method, headerPairsFromHeaders(request.headers), body); + // "follow" (the default), "manual" -- hand back the 3xx itself -- or + // "error", which rejects rather than following. + var mode = init.redirect !== undefined ? init.redirect : (request.redirect || "follow"); + if (mode !== "follow" && mode !== "manual" && mode !== "error") + return Promise.reject(new TypeError("fetch: redirect must be follow, manual or error")); + var raw = __sxnFetchRaw(request.url, request.method, headerPairsFromHeaders(request.headers), body, mode); if (signal) signal.addEventListener("abort", raw.stream.__abort.bind(raw.stream)); return raw.promise.then(function (head) { + if (mode === "error" && head.status >= 300 && head.status < 400 && head.status !== 304) + throw new TypeError("fetch: the server redirected and redirect is 'error'"); var headers = new Headers(); for (var i = 0; i < head.headers.length; i += 2) headers.append(head.headers[i], head.headers[i + 1]); - return new Response(raw.stream, { status: head.status, statusText: head.statusText, headers: headers, url: head.url }); + var response = new Response(raw.stream, { status: head.status, statusText: head.statusText, headers: headers, url: head.url }); + response.redirected = mode === "follow" && head.url !== request.url; + return response; }); }; @@ -984,6 +1009,9 @@ this._closedDeferred = deferred(); this._disturbed = false; const c = new ReadableStreamDefaultController(this, source, strategy); + // A byte stream's controller answers to the name the spec gives it. The + // queue underneath is the same one: chunks are views either way. + if (source.type === "bytes") Object.setPrototypeOf(c, ReadableByteStreamController.prototype); this._controller = c; const started = typeof source.start === "function" ? Promise.resolve().then(() => source.start(c)) : Promise.resolve(); @@ -994,9 +1022,8 @@ get() { return this._reader !== null; }, }); ReadableStream.prototype.getReader = function (options) { - if (options && options.mode === "byob") - throw new TypeError("byob readers are not supported"); streamAssert(!this.locked, "stream is already locked"); + if (options && options.mode === "byob") return new ReadableStreamBYOBReader(this); return new ReadableStreamDefaultReader(this); }; ReadableStream.prototype.cancel = function (reason) { @@ -1300,16 +1327,27 @@ }; globalThis.WritableStreamDefaultWriter = WritableStreamDefaultWriter; + // The handle a transformer is given. It reaches the readable side's own + // controller through a getter, because that one does not exist yet when + // this is constructed. + function TransformStreamDefaultController(readable) { + Object.defineProperty(this, "_readable", { value: readable }); + } + TransformStreamDefaultController.prototype.enqueue = function (chunk) { this._readable().enqueue(chunk); }; + TransformStreamDefaultController.prototype.error = function (e) { this._readable().error(e); }; + TransformStreamDefaultController.prototype.terminate = function () { + try { this._readable().close(); } catch {} + }; + Object.defineProperty(TransformStreamDefaultController.prototype, "desiredSize", { + get() { return this._readable().desiredSize; }, + }); + globalThis.TransformStreamDefaultController = TransformStreamDefaultController; + // ---- transform ---- function TransformStream(transformer, writableStrategy, readableStrategy) { transformer = transformer || {}; let readableController; - const controller = { - enqueue: (chunk) => readableController.enqueue(chunk), - error: (e) => { readableController.error(e); }, - terminate: () => { try { readableController.close(); } catch {} }, - get desiredSize() { return readableController.desiredSize; }, - }; + const controller = new TransformStreamDefaultController(() => readableController); this.readable = new ReadableStream({ start(c) { readableController = c; }, }, readableStrategy || {}); @@ -1367,6 +1405,358 @@ } globalThis.TextDecoderStream = TextDecoderStream; + // ---- byte streams ---- + // The queue is the same one the default controller keeps; what is different + // is the reader, which copies out of the queued views into the caller's + // buffer. A byobRequest is never handed to a source, because nothing here + // reads straight into the caller's memory -- so it reads null, and a source + // that only writes through one will find nothing to write into. + function ReadableByteStreamController() { throw new TypeError("Illegal constructor"); } + ReadableByteStreamController.prototype = Object.create(ReadableStreamDefaultController.prototype); + ReadableByteStreamController.prototype.constructor = ReadableByteStreamController; + Object.defineProperty(ReadableByteStreamController.prototype, "byobRequest", { get() { return null; } }); + globalThis.ReadableByteStreamController = ReadableByteStreamController; + + function ReadableStreamBYOBRequest() { throw new TypeError("Illegal constructor"); } + globalThis.ReadableStreamBYOBRequest = ReadableStreamBYOBRequest; + + function ReadableStreamBYOBReader(stream) { + ReadableStreamDefaultReader.call(this, stream); + this._left = null; + } + ReadableStreamBYOBReader.prototype = Object.create(ReadableStreamDefaultReader.prototype); + ReadableStreamBYOBReader.prototype.constructor = ReadableStreamBYOBReader; + ReadableStreamBYOBReader.prototype.read = function (view) { + if (!ArrayBuffer.isView(view)) return Promise.reject(new TypeError("read needs a view")); + if (view.byteLength === 0) return Promise.reject(new TypeError("view is empty")); + const self = this; + const fill = (source) => { + const room = view.byteLength; + const take = Math.min(room, source.byteLength); + new Uint8Array(view.buffer, view.byteOffset, room) + .set(source.subarray(0, take)); + self._left = take < source.byteLength ? source.subarray(take) : null; + const Kind = view.constructor; + return { value: new Kind(view.buffer, view.byteOffset, take / (view.BYTES_PER_ELEMENT || 1)), done: false }; + }; + if (this._left) return Promise.resolve(fill(this._left)); + return ReadableStreamDefaultReader.prototype.read.call(this).then((r) => { + if (r.done) return { value: new view.constructor(view.buffer, view.byteOffset, 0), done: true }; + const bytes = ArrayBuffer.isView(r.value) + ? new Uint8Array(r.value.buffer, r.value.byteOffset, r.value.byteLength) + : new Uint8Array(r.value); + return fill(bytes); + }); + }; + globalThis.ReadableStreamBYOBReader = ReadableStreamBYOBReader; + globalThis.ReadableStreamDefaultController = ReadableStreamDefaultController; + globalThis.WritableStreamDefaultController = WritableStreamDefaultController; + + // ---- CompressionStream / DecompressionStream ---- + // A TransformStream over the streaming zlib in src/node.c: one z_stream per + // stream object, chunks pushed through as they arrive. + const zlibWindow = { gzip: 31, deflate: 15, "deflate-raw": -15 }; + function compressionTransform(format, decompress) { + const bits = zlibWindow[format]; + if (bits === undefined) throw new TypeError("Unsupported compression format: " + format); + const handle = __sxnZlibStreamNew(bits, -1, decompress); + const stream = new TransformStream({ + transform(chunk, controller) { + const bytes = ArrayBuffer.isView(chunk) + ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) + : new Uint8Array(chunk); + const out = __sxnZlibStreamPush(handle, bytes, false); + if (out.length) controller.enqueue(out); + }, + flush(controller) { + const out = __sxnZlibStreamPush(handle, null, true); + if (out.length) controller.enqueue(out); + }, + }); + return stream; + } + function CompressionStream(format) { + const t = compressionTransform(format, false); + this.readable = t.readable; + this.writable = t.writable; + } + function DecompressionStream(format) { + const t = compressionTransform(format, true); + this.readable = t.readable; + this.writable = t.writable; + } + globalThis.CompressionStream = CompressionStream; + globalThis.DecompressionStream = DecompressionStream; + + // ---- the rest of the Minimum Common API's names ---- + function ErrorEvent(type, init) { + Event.call(this, type, init); + init = init || {}; + this.message = init.message === undefined ? "" : String(init.message); + this.filename = init.filename === undefined ? "" : String(init.filename); + this.lineno = init.lineno || 0; + this.colno = init.colno || 0; + this.error = init.error; + } + ErrorEvent.prototype = Object.create(Event.prototype); + ErrorEvent.prototype.constructor = ErrorEvent; + globalThis.ErrorEvent = ErrorEvent; + + function PromiseRejectionEvent(type, init) { + Event.call(this, type, init); + init = init || {}; + this.promise = init.promise; + this.reason = init.reason; + } + PromiseRejectionEvent.prototype = Object.create(Event.prototype); + PromiseRejectionEvent.prototype.constructor = PromiseRejectionEvent; + globalThis.PromiseRejectionEvent = PromiseRejectionEvent; + + // reportError reports an exception the way an uncaught one is reported, + // without unwinding the caller. + globalThis.reportError = function (error) { + const text = error instanceof Error && error.stack ? error.stack : String(error); + __sxnWriteStderr("Uncaught " + text + "\n"); + }; + + // A worker-shaped runtime names its own global `self`. + if (typeof globalThis.self === "undefined") globalThis.self = globalThis; + + function Performance() { throw new TypeError("Illegal constructor"); } + Performance.prototype.now = function () { return __sxnNow(); }; + Object.defineProperty(Performance.prototype, "timeOrigin", { get() { return 0; } }); + Performance.prototype.toJSON = function () { return { timeOrigin: 0 }; }; + globalThis.Performance = Performance; + Object.setPrototypeOf(globalThis.performance, Performance.prototype); + + // ---------------- global event handlers ---------------- + // The global object is an event target: `addEventListener("error", ...)` + // and the matching `onerror` property both work, and both see the same + // events. The C side (src/main.c, src/network.c) calls in here. + if (typeof globalThis.addEventListener !== "function") { + const target = new EventTarget(); + globalThis.addEventListener = EventTarget.prototype.addEventListener.bind(target); + globalThis.removeEventListener = EventTarget.prototype.removeEventListener.bind(target); + globalThis.dispatchEvent = EventTarget.prototype.dispatchEvent.bind(target); + } + for (const name of ["onerror", "onunhandledrejection", "onrejectionhandled"]) { + let handler = null; + Object.defineProperty(globalThis, name, { + configurable: true, + get() { return handler; }, + set(fn) { handler = typeof fn === "function" ? fn : null; }, + }); + } + function fireGlobal(event, handlerName) { + let handled = false; + try { handled = globalThis.dispatchEvent(event) === false; } catch {} + const handler = globalThis[handlerName]; + if (handler) { + try { + const r = handler.call(globalThis, event); + // onerror is the odd one: returning true from it is what cancels it. + if (r === true || event.defaultPrevented) handled = true; + } catch {} + } + return handled || event.defaultPrevented; + } + // Called with the exception that reached the top; true means it was + // handled and should not be printed. + globalThis.__sxnUncaught = function (error) { + const event = new ErrorEvent("error", { + cancelable: true, + message: error instanceof Error ? error.message : String(error), + error, + }); + return fireGlobal(event, "onerror"); + }; + // Rejections are collected as they happen and reported once every job that + // could still have handled one has run. + const pendingRejections = []; + const reportedRejections = []; + globalThis.__sxnRejectionRaised = function (promise, reason) { + if (!pendingRejections.some((e) => e.promise === promise)) + pendingRejections.push({ promise, reason }); + }; + globalThis.__sxnRejectionHandled = function (promise) { + const i = pendingRejections.findIndex((e) => e.promise === promise); + if (i >= 0) pendingRejections.splice(i, 1); + const j = reportedRejections.findIndex((e) => e.promise === promise); + if (j >= 0) { + const entry = reportedRejections.splice(j, 1)[0]; + fireGlobal(new PromiseRejectionEvent("rejectionhandled", { + cancelable: true, promise, reason: entry.reason, + }), "onrejectionhandled"); + } + }; + globalThis.__sxnFlushRejections = function () { + while (pendingRejections.length) { + const entry = pendingRejections.shift(); + reportedRejections.push(entry); + fireGlobal(new PromiseRejectionEvent("unhandledrejection", { + cancelable: true, promise: entry.promise, reason: entry.reason, + }), "onunhandledrejection"); + } + }; + + // ---------------- URLPattern ---------------- + // Each component of a URL gets its own compiled pattern: `:name` captures a + // segment, `(...)` is a regular expression written in place, `*` is + // anything, and `{...}` groups what it wraps so a `?` after it can make the + // whole thing optional. What a bare `:name` or `*` will match stops at the + // separator its component uses -- `/` in a path, `.` in a hostname -- which + // is what makes `/books/:id` match one segment rather than the rest. + const patternSeparator = { pathname: "/", hostname: "." }; + function compileComponent(pattern, kind) { + if (pattern === undefined || pattern === null || pattern === "*") + return { source: "^.*$", names: [], wildcard: true }; + const text = String(pattern); + const sep = patternSeparator[kind]; + const segment = sep ? "[^" + (sep === "." ? "." : "\\/") + "]+?" : "[^]+?"; + const names = []; + let out = ""; + let unnamed = 0; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + if (ch === "\\") { out += escapeRe(text[++i] || ""); continue; } + if (ch === "*") { names.push(String(unnamed++)); out += "(.*)"; continue; } + if (ch === ":") { + let name = ""; + while (i + 1 < text.length && /[A-Za-z0-9_$]/.test(text[i + 1])) name += text[++i]; + let body = segment; + if (text[i + 1] === "(") { + const close = matchParen(text, i + 1); + body = text.slice(i + 2, close); + i = close; + } + names.push(name); + out += "(" + body + ")"; + const mod = text[i + 1]; + if (mod === "?" || mod === "*" || mod === "+") { out += mod; i++; } + continue; + } + if (ch === "(") { + const close = matchParen(text, i); + names.push(String(unnamed++)); + out += "(" + text.slice(i + 1, close) + ")"; + i = close; + continue; + } + if (ch === "{") { + const close = text.indexOf("}", i); + const inner = compileComponent(text.slice(i + 1, close < 0 ? text.length : close), kind); + out += "(?:" + inner.source.slice(1, -1) + ")"; + for (const n of inner.names) names.push(n); + i = close < 0 ? text.length : close; + const mod = text[i + 1]; + if (mod === "?" || mod === "*" || mod === "+") { out += mod; i++; } + continue; + } + out += escapeRe(ch); + } + return { source: "^" + out + "$", names, wildcard: false }; + } + function escapeRe(ch) { return /[.*+?^${}()|[\]\\\/]/.test(ch) ? "\\" + ch : ch; } + function matchParen(text, start) { + let depth = 0; + for (let i = start; i < text.length; i++) { + if (text[i] === "\\") { i++; continue; } + if (text[i] === "(") depth++; + else if (text[i] === ")" && --depth === 0) return i; + } + return text.length; + } + + const patternParts = ["protocol", "username", "password", "hostname", "port", + "pathname", "search", "hash"]; + // A pattern written as one string is split the way a URL is, without + // resolving it: the pieces are patterns, not values, so they cannot be + // handed to the URL parser. + function splitPatternString(text) { + const out = {}; + let rest = String(text); + const scheme = rest.match(/^([^:\/?#]+):\/\//); + if (scheme) { out.protocol = scheme[1]; rest = rest.slice(scheme[0].length); } + const hash = rest.indexOf("#"); + if (hash >= 0) { out.hash = rest.slice(hash + 1); rest = rest.slice(0, hash); } + const search = rest.indexOf("?"); + if (search >= 0) { out.search = rest.slice(search + 1); rest = rest.slice(0, search); } + if (scheme) { + const slash = rest.indexOf("/"); + let authority = slash < 0 ? rest : rest.slice(0, slash); + out.pathname = slash < 0 ? "*" : rest.slice(slash); + const at = authority.lastIndexOf("@"); + if (at >= 0) { + const creds = authority.slice(0, at).split(":"); + out.username = creds[0]; + if (creds.length > 1) out.password = creds[1]; + authority = authority.slice(at + 1); + } + const colon = authority.lastIndexOf(":"); + if (colon > 0 && authority.indexOf("}", colon) < 0) { + out.port = authority.slice(colon + 1); + authority = authority.slice(0, colon); + } + out.hostname = authority; + } else { + out.pathname = rest; + } + return out; + } + function URLPattern(input, baseURL) { + const raw = typeof input === "string" ? splitPatternString(input) : Object.assign({}, input || {}); + if (typeof baseURL === "string" || (input && typeof input === "object" && input.baseURL)) { + const base = new URL(typeof baseURL === "string" ? baseURL : input.baseURL); + if (raw.protocol === undefined) raw.protocol = base.protocol.replace(":", ""); + if (raw.hostname === undefined) raw.hostname = base.hostname; + if (raw.port === undefined && base.port) raw.port = base.port; + } + this._compiled = {}; + for (const part of patternParts) { + this[part] = raw[part] === undefined ? "*" : String(raw[part]); + this._compiled[part] = compileComponent(raw[part], part); + } + } + Object.defineProperty(URLPattern.prototype, "hasRegExpGroups", { + get() { return patternParts.some((p) => /[(:]/.test(this[p])); }, + }); + function patternInputParts(input, base) { + if (typeof input === "string" || input instanceof URL) { + let url; + try { url = new URL(String(input), base); } catch { return null; } + return { + protocol: url.protocol.replace(/:$/, ""), + username: url.username, password: url.password, + hostname: url.hostname, port: url.port, + pathname: url.pathname, + search: url.search.replace(/^\?/, ""), + hash: url.hash.replace(/^#/, ""), + }; + } + const out = {}; + for (const part of patternParts) out[part] = input && input[part] !== undefined ? String(input[part]) : ""; + return out; + } + URLPattern.prototype.exec = function (input, base) { + const values = patternInputParts(input, base); + if (!values) return null; + const result = { inputs: base === undefined ? [input] : [input, base] }; + for (const part of patternParts) { + const compiled = this._compiled[part]; + const value = values[part]; + // An unwritten component matches anything, including the empty string a + // URL leaves behind for a port or a hash it does not have. + const m = new RegExp(compiled.source).exec(value); + if (!m) return null; + const groups = {}; + compiled.names.forEach((name, i) => { groups[name] = m[i + 1]; }); + result[part] = { input: value, groups }; + } + return result; + }; + URLPattern.prototype.test = function (input, base) { return this.exec(input, base) !== null; }; + globalThis.URLPattern = URLPattern; + // ---------------- File / FormData ---------------- // File is a Blob with a name and a modified time; FormData is the multi-map // fetch bodies and form parsing are built on. @@ -1559,7 +1949,7 @@ if (typeof globalThis.navigator === "undefined") { globalThis.navigator = Object.freeze({ userAgent: "sxn/" + (Sxn && Sxn.version ? Sxn.version : "0"), - // WinterCG names this for runtime detection. + // WinterTC names this for runtime detection. platform: "", }); } @@ -1606,7 +1996,7 @@ // node:http is built directly on it through __sxnServe, and Node's own // req.url is a path, not an absolute URL -- but it is not what // spec/RUNTIME.md documents or what a handler written for any other - // WinterCG runtime expects. Returning a Response used to write a garbled + // WinterTC runtime expects. Returning a Response used to write a garbled // reply, and `new URL(req.url)` threw, so the documented example did not // run. This adapts both ends in one place. (function () { @@ -1615,14 +2005,44 @@ function toRequest(raw, origin) { // Node hands a handler the path; the Fetch standard requires an - // absolute URL, and the Host header is what makes it absolute. - var host = (raw.headers && raw.headers.host) || origin; - var url = new URL(raw.url || "/", "http://" + String(host).replace(/^https?:\/\//, "")); - var init = { method: raw.method || "GET", headers: raw.headers || {} }; - // A GET/HEAD request may not carry a body, and the native layer sends - // "" rather than nothing when there is none. - if (raw.body !== undefined && raw.body !== null && raw.body !== "") init.body = raw.body; - return new Request(url.href, init); + // absolute URL, and the Host header is what makes it absolute. Built by + // concatenation rather than `new URL`: this runs per request, and the + // handler that wants the parsed form parses it itself. + var path = raw.url || "/"; + var href; + if (path.charCodeAt(0) === 47 /* "/" */) { + var host = (raw.headers && raw.headers.host) || origin; + href = "http://" + host + path; + } else { + href = new URL(path, "http://" + origin).href; + } + var init = { method: raw.method || "GET" }; + // The body as the bytes the native layer already has, rather than a + // string copied out of them: a megabyte of JSON is not turned into a + // megabyte of JavaScript string on the way to JSON.parse. + if (raw.bodyBytes !== undefined && raw.bodyLength > 0) { + init.body = raw.bodyBytes.subarray(raw.bodyOffset, raw.bodyOffset + raw.bodyLength); + } else if (raw.body !== undefined && raw.body !== null && raw.body !== "") { + init.body = raw.body; + } + var request = new Request(href, init); + // The headers are built on first read. Copying every header into a + // Headers list costs about a microsecond a request, and a handler that + // only routes on the method and the path never looks at them. + var rawHeaders = raw.headers || {}; + function settle(value) { + Object.defineProperty(request, "headers", { + value: value, writable: true, enumerable: true, configurable: true, + }); + return value; + } + Object.defineProperty(request, "headers", { + enumerable: true, + configurable: true, + get: function () { return settle(new Headers(rawHeaders)); }, + set: settle, + }); + return request; } function toNative(result) { @@ -1640,15 +2060,18 @@ headers[name] = value; } }); + // A HEAD response reports the length its body would have had and + // sends none of it. + var omit = result.status !== 204 && result.status !== 304 && result.bodyOmitted === true; // Hand a text body over as a string so the native layer's own // text/plain default applies; bytes go over as bytes. var body = result._staticBody !== undefined ? result._staticBody : null; if (body === null) { return result.arrayBuffer().then(function (buf) { - return { statusCode: result.status, headers: headers, body: new Uint8Array(buf) }; + return { statusCode: result.status, headers: headers, body: new Uint8Array(buf), bodyOmitted: omit }; }); } - return { statusCode: result.status, headers: headers, body: body }; + return { statusCode: result.status, headers: headers, body: body, bodyOmitted: omit }; } Sxn.serve = function serve(options, handler) { @@ -1660,4 +2083,28 @@ return handle; }; })(); + + // ---------------- console.error and friends ---------------- + // QuickJS installs console.log and nothing else, so ported code that logs a + // warning or an error died on a missing function. error/warn go to stderr + // (__sxnWriteStderr, the one JS-visible write to fd 2), which is also what + // process.stderr is built on, so a diagnostic never lands in the program's + // own stdout. + (function () { + function format(args) { + var parts = []; + for (var i = 0; i < args.length; i++) { + var value = args[i]; + if (typeof value === "string") { parts.push(value); continue; } + if (value instanceof Error) { parts.push(String(value.stack || value)); continue; } + try { parts.push(JSON.stringify(value) ?? String(value)); } + catch (e) { parts.push(String(value)); } + } + return parts.join(" "); + } + console.error = function error() { __sxnWriteStderr(format(Array.prototype.slice.call(arguments)) + "\n"); }; + console.warn = console.error; + console.info = console.log; + console.debug = console.log; + })(); })(); diff --git a/src/main.c b/src/main.c index 1f969a2..23196c3 100644 --- a/src/main.c +++ b/src/main.c @@ -494,6 +494,58 @@ static char *sxn_pkg_entry(JSContext *ctx, const char *pkg_dir) { return out; } +/* An uncaught exception, and a promise rejection nothing ever handled, are + both reportable events before they are fatal: the page-style handlers + (onerror, onunhandledrejection, onrejectionhandled) get first refusal, and + only what they decline is printed. The dispatch itself is JavaScript, in + src/bootstrap.js; this is the wiring. */ +static bool sxn_dispatch_uncaught(JSContext *ctx) { + JSValue error = JS_GetException(ctx); + JSValue global = JS_GetGlobalObject(ctx); + JSValue fn = JS_GetPropertyStr(ctx, global, "__sxnUncaught"); + bool handled = false; + if (JS_IsFunction(ctx, fn)) { + JSValueConst args[1] = { error }; + JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, 1, args); + if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx)); + else handled = JS_ToBool(ctx, r); + JS_FreeValue(ctx, r); + } + JS_FreeValue(ctx, fn); + JS_FreeValue(ctx, global); + if (!handled) { + JS_Throw(ctx, error); /* put it back for the usual report */ + return false; + } + JS_FreeValue(ctx, error); + return true; +} + +static void sxn_report_uncaught(JSContext *ctx) { + if (!sxn_dispatch_uncaught(ctx)) js_std_dump_error(ctx); +} + +/* Both halves of the rejection tracker are handed to JavaScript, which keeps + the list and decides when a rejection has gone unhandled for good. */ +extern int sxn_rejections_pending; /* src/network.c, read by its loop */ + +static void sxn_rejection_tracker(JSContext *ctx, JSValueConst promise, + JSValueConst reason, bool is_handled, void *opaque) { + (void)opaque; + if (!is_handled) sxn_rejections_pending = 1; + JSValue global = JS_GetGlobalObject(ctx); + JSValue fn = JS_GetPropertyStr(ctx, global, + is_handled ? "__sxnRejectionHandled" : "__sxnRejectionRaised"); + if (JS_IsFunction(ctx, fn)) { + JSValueConst args[2] = { promise, reason }; + JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, 2, args); + if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx)); + JS_FreeValue(ctx, r); + } + JS_FreeValue(ctx, fn); + JS_FreeValue(ctx, global); +} + static char *sxn_module_normalize(JSContext *ctx, const char *base_name, const char *name, void *opaque); @@ -832,14 +884,15 @@ static char *sxn_module_normalize(JSContext *ctx, const char *base_name, static JSModuleDef *sxn_module_loader(JSContext *ctx, const char *name, void *opaque, JSValueConst attributes) { - /* node:buffer/path/events/process/fs/fs-promises are pre-registered by - sxn_install_node_compat via JS_NewCModule (same mechanism as - qjs:std/qjs:os/qjs:bjson below), so `import ... from "node:xxx"` - resolves to them without ever reaching this loader. Only an - unregistered node: specifier gets here -- report it clearly instead - of falling through to file-based resolution, which would otherwise - try (and fail confusingly) to open a file literally named "node:xxx". */ + /* A node: specifier is registered here, on the way through, rather than + at startup: JS_NewCModule plus an atom per export name is real work, + and a program that imports two builtins used to pay for all of them. + A name this runtime does not have is reported clearly instead of + falling through to file-based resolution, which would otherwise try + (and fail confusingly) to open a file literally named "node:xxx". */ if (has_prefix(name, "node:")) { + JSModuleDef *m = sxn_node_module_load(ctx, name); + if (m) return m; JS_ThrowReferenceError(ctx, "unsupported node: module '%s'", name); return NULL; } @@ -921,7 +974,7 @@ static int execute_file(int argc, char **argv, const char *filename, goto failure; } JS_SetModuleLoaderFunc2(runtime, sxn_module_normalize, sxn_module_loader, js_module_check_attributes, NULL); - JS_SetHostPromiseRejectionTracker(runtime, js_std_promise_rejection_tracker, NULL); + JS_SetHostPromiseRejectionTracker(runtime, sxn_rejection_tracker, NULL); bool is_sxbc = suffix(filename, ".sxbc"); if (!is_sxbc) { @@ -996,13 +1049,13 @@ static int execute_file(int argc, char **argv, const char *filename, it has to drive the uv loop too, or any await on a timer, a fetch or a server never resumes. */ if (!JS_IsException(value)) value = sxn_await_with_loop(context, value); - if (JS_IsException(value)) { js_std_dump_error(context); JS_FreeValue(context, value); goto failure; } + if (JS_IsException(value)) { sxn_report_uncaught(context); JS_FreeValue(context, value); goto failure; } JS_FreeValue(context, value); - if (js_std_loop(context)) { js_std_dump_error(context); goto failure; } + if (js_std_loop(context)) { sxn_report_uncaught(context); goto failure; } /* Drains any server sockets / async file reads registered by network.c; a no-op that returns immediately for scripts that never called Sxn.serve or Sxn.file(...).text(). */ - if (sxn_run_event_loop(context)) { js_std_dump_error(context); goto failure; } + if (sxn_run_event_loop(context)) { sxn_report_uncaught(context); goto failure; } if (memory_report) { JSMemoryUsage usage; JS_ComputeMemoryUsage(runtime, &usage); @@ -1049,6 +1102,12 @@ static int sxn_compile_command(int argc, char **argv) { js_std_init_handlers(runtime); JSContext *context = JS_NewContext(runtime); if (!context) { JS_FreeRuntime(runtime); return 2; } + /* A module's imports are resolved while it is compiled, so compiling + needs the same loader running a file does. Without it, `sxn compile` + failed on any file with a relative import -- which is every file in a + program of more than one. */ + JS_SetModuleLoaderFunc2(runtime, sxn_module_normalize, sxn_module_loader, + js_module_check_attributes, NULL); int rc = sxn_compile_file(context, in, out, strip); js_std_free_handlers(runtime); JS_FreeContext(context); @@ -1058,7 +1117,7 @@ static int sxn_compile_command(int argc, char **argv) { } static void usage(void) { - puts("SXN 0.0.1\n" + puts("SXN 0.0.2\n" "Usage:\n" " sxn [args...]\n" " sxn run [script] -- [args...]\n" @@ -1068,6 +1127,7 @@ static void usage(void) { " sxn remove package\n" " sxn init\n" " sxn [--memory-report] [--leak-check] [--compile-cache] [args...]\n" + " sxn [--no-idle-gc] [--idle-gc-floor=] -- cycle sweeping while the event loop is quiet\n" " sxn compile [-o out.sxbc] [--strip] -- compile to bytecode for distribution\n" " sxn [args...] -- run precompiled bytecode directly\n" " sxn lsp --stdio\n" @@ -1085,22 +1145,37 @@ int main(int argc, char **argv) { _setmode(_fileno(stdin), _O_BINARY); #endif if (argc < 2 || !strcmp(argv[1], "--help") || !strcmp(argv[1], "-h")) { usage(); return 0; } - if (!strcmp(argv[1], "--version") || !strcmp(argv[1], "-v")) { puts("sxn 0.0.1"); return 0; } + if (!strcmp(argv[1], "--version") || !strcmp(argv[1], "-v")) { puts("sxn 0.0.2"); return 0; } if (!strcmp(argv[1], "lsp")) return sxn_lsp_main(); if (!strcmp(argv[1], "compile")) return sxn_compile_command(argc, argv); if (!strcmp(argv[1], "run") || !strcmp(argv[1], "install") || !strcmp(argv[1], "add") || !strcmp(argv[1], "remove") || !strcmp(argv[1], "init")) return sxn_package_command(argc, argv); bool memory_report = false, leak_check = false, compile_cache = false; + int idle_gc = 1; + size_t idle_gc_floor = 0; /* 0 keeps sxn_configure_idle_gc's own default */ int file_index = 1; while (file_index < argc && (!strcmp(argv[file_index], "--memory-report") || !strcmp(argv[file_index], "--leak-check") || + !strcmp(argv[file_index], "--no-idle-gc") || + !strncmp(argv[file_index], "--idle-gc-floor=", 16) || !strcmp(argv[file_index], "--compile-cache"))) { if (!strcmp(argv[file_index], "--memory-report")) memory_report = true; else if (!strcmp(argv[file_index], "--leak-check")) leak_check = true; + else if (!strcmp(argv[file_index], "--no-idle-gc")) idle_gc = 0; + else if (!strncmp(argv[file_index], "--idle-gc-floor=", 16)) { + char *end = NULL; + long long mb = strtoll(argv[file_index] + 16, &end, 10); + if (!end || *end || mb < 0) { + fprintf(stderr, "sxn: --idle-gc-floor expects a size in MB\n"); + return 2; + } + idle_gc_floor = (size_t)mb * 1024u * 1024u; + } else compile_cache = true; ++file_index; } if (file_index >= argc) { usage(); return 2; } + sxn_configure_idle_gc(idle_gc, idle_gc_floor); /* An argument that names a real file is a file to run, whatever it is called. Node and Bun both run `./node_modules/.bin/whatever`, and every CLI shipped by an npm package is extensionless, so requiring a known diff --git a/src/network.c b/src/network.c index 874c88e..29e1dcb 100644 --- a/src/network.c +++ b/src/network.c @@ -3,8 +3,21 @@ #include #include #include +#include +#include #include #include +/* UV_TCP_REUSEPORT arrived in libuv 1.49; older versions get the socket + option set by hand below. */ +#if UV_VERSION_MAJOR > 1 || (UV_VERSION_MAJOR == 1 && UV_VERSION_MINOR >= 49) +#define SXN_UV_HAS_REUSEPORT 1 +#else +#define SXN_UV_HAS_REUSEPORT 0 +#endif +#ifndef _WIN32 +#include +#include +#endif #include "sxfe.h" #include "sxn_bootstrap.h" @@ -131,12 +144,22 @@ static int sxn_strcasecmp(const char *a, const char *b) { return (unsigned char)*a - (unsigned char)*b; } +/* Make room for `want` bytes in one go. A 1MB request body arriving through + a buffer that doubles from 256 bytes is copied about a dozen times on the + way in; when Content-Length says how big it will be, it is copied once. */ +static void dynbuf_reserve(DynBuf *buf, size_t want) { + if (want <= buf->cap) return; + /* Grow geometrically as well as to what was asked for: the read path + asks for "what I have plus another read", and honouring that exactly + would copy the whole buffer on every read. */ + size_t cap = buf->cap ? buf->cap * 2 : 256; + if (cap < want) cap = want; + buf->data = realloc(buf->data, cap); + buf->cap = cap; +} + static void dynbuf_append(DynBuf *buf, const void *data, size_t n) { - if (buf->length + n > buf->cap) { - size_t cap = buf->cap ? buf->cap * 2 : 256; - while (cap < buf->length + n) cap *= 2; - buf->data = realloc(buf->data, cap); buf->cap = cap; - } + dynbuf_reserve(buf, buf->length + n); memcpy(buf->data + buf->length, data, n); buf->length += n; } static void dynbuf_puts(DynBuf *buf, const char *s) { dynbuf_append(buf, s, strlen(s)); } @@ -162,12 +185,50 @@ static const char *reason(int status) { default: return "Response"; } } +/* The floor a sweep will not reduce the GC threshold below, and the initial + threshold main.c sets. Kept in one place because the idle sweep and + Sxn.gc() both have to restore it (see sxn_sweep_cycles). */ +#define SXN_GC_THRESHOLD_FLOOR (8u * 1024u * 1024u) + +/* Collect cycles, then put the allocation threshold back where the amount now + live says it belongs. + + The second half is the part that matters. QuickJS only ever collects from + js_trigger_gc, which fires when an allocation would cross + malloc_gc_threshold and then sets the next threshold to 1.5x whatever was + live at that moment. So a burst that dies right after a collection leaves + the threshold sized for the peak and nothing lowers it again: the garbage + is unreachable, but the bar for noticing has been raised above it, and the + bigger the garbage the higher the bar. JS_RunGC reclaims without touching + the threshold, so a sweep that does not also reset it fixes one burst and + leaves the ratchet in place for the next. */ +static size_t sxn_sweep_cycles(JSRuntime *rt) { + size_t live, threshold; + JS_RunGC(rt); + live = JS_GetMallocSize(rt); + threshold = live + (live >> 1); + if (threshold < SXN_GC_THRESHOLD_FLOOR) threshold = SXN_GC_THRESHOLD_FLOOR; + JS_SetGCThreshold(rt, threshold); + return live; +} + +static JSValue sxn_gc(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + /* The emit memo holds strong references to one emitter's _events object, + its event name and its listener list, so it is a root the sweep cannot + see past. Releasing it first is what lets that graph go. */ + sxn_free_ee_memo(ctx); + return JS_NewInt64(ctx, (int64_t)sxn_sweep_cycles(JS_GetRuntime(ctx))); +} + static JSValue sxn_memory_usage(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; JSRuntime *rt = JS_GetRuntime(ctx); JSMemoryUsage mem; JSGCStats gc; + size_t rss = 0; JS_ComputeMemoryUsage(rt, &mem); JS_GetGCStats(rt, &gc); JSValue result = JS_NewObject(ctx); @@ -186,6 +247,12 @@ static JSValue sxn_memory_usage(JSContext *ctx, JSValueConst this_val, MEM_FIELD("gcTotalNs", gc.total_ns); MEM_FIELD("gcLastNs", gc.last_ns); MEM_FIELD("gcMaxNs", gc.max_ns); + /* Every field above is the engine allocator's own accounting. `rss` is + the operating system's, which is the only one that answers "is this + process growing": the allocator can hold freed pages rather than + return them, so the two move independently. Reported, never asserted + on -- see tests/fixtures/leak_cycle_stress.sx. */ + if (uv_resident_set_memory(&rss) == 0) MEM_FIELD("rss", rss); #undef MEM_FIELD return result; } @@ -225,16 +292,33 @@ static void websocket_text(DynBuf *out, const char *text) { /* Shared by every accepted connection: the JS handler is looked up once per Sxn.serve() call and kept alive (JS_DupValue) for the server's lifetime. */ -typedef struct ServeState { JSContext *ctx; JSValue handler; } ServeState; +typedef struct ConnState ConnState; +typedef struct ServeState { JSContext *ctx; JSValue handler; ConnState *conns; } ServeState; /* Per-connection state: outlives the read/parse/dispatch step so the assembled response survives until the uv_write completes. */ -typedef struct ConnState { +struct ConnState { ServeState *serve; uv_tcp_t handle; char *write_data; /* Held across an async handler: the upgrade bits belong to the request that is still being answered, and the read buffer is long gone. */ char *pending_upgrade; char *pending_ws_key; -} ConnState; + /* A request arrives over as many reads as the kernel feels like giving + us; only a small one fits in the first. This accumulates them until + the head and Content-Length bytes of body are both in hand. */ + DynBuf in; + /* Keep-alive: whether this connection survives the response being + written, and how many bytes of `in` the answered request used -- what + is left after them is the next, pipelined request. */ + int keep_alive; size_t consumed; + /* Every live connection of a server, so stop() can close them: a + keep-alive connection outlives the request that opened it, and would + otherwise hold the loop open after its server was stopped. */ + ConnState *prev, *next; +}; + +/* A request bigger than this is refused rather than buffered: the whole + thing is held in memory before the handler sees it. */ +#define SXN_MAX_REQUEST_BYTES (64u * 1024u * 1024u) static void conn_deliver(JSContext *ctx, ConnState *conn, JSValue result, const char *upgrade, const char *ws_key); @@ -267,20 +351,60 @@ static JSValue conn_promise_fail(JSContext *ctx, JSValueConst this_val, return JS_UNDEFINED; } +/* Unlink first, then close: the close callback may run after the server it + belonged to is gone. */ +static void conn_shutdown(ConnState *conn, uv_close_cb cb) { + if (conn->serve) { + if (conn->prev) conn->prev->next = conn->next; else conn->serve->conns = conn->next; + if (conn->next) conn->next->prev = conn->prev; + conn->serve = NULL; conn->prev = conn->next = NULL; + } + uv_close((uv_handle_t *)&conn->handle, cb); +} + static void conn_close_cb(uv_handle_t *handle) { ConnState *conn = (ConnState *)handle->data; - free(conn->write_data); free(conn); + free(conn->write_data); free(conn->in.data); free(conn); } +static void conn_try_dispatch(ConnState *conn); +static void conn_alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf); +static void conn_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf); + +/* One response is written. On a keep-alive exchange the connection is reused + for the next request rather than closed -- without this every request paid + for a new TCP connection, which under a load test exhausted the client's + ephemeral ports long before the server was the bottleneck. */ static void conn_write_cb(uv_write_t *req, int status) { - (void)status; ConnState *conn = (ConnState *)req->data; free(req); - uv_close((uv_handle_t *)&conn->handle, conn_close_cb); -} - + if (status < 0 || !conn->keep_alive) { + conn_shutdown(conn, conn_close_cb); + return; + } + free(conn->write_data); conn->write_data = NULL; + /* Whatever follows the request just answered is the next one, already + here: a pipelining client sends without waiting. */ + size_t left = conn->in.length > conn->consumed ? conn->in.length - conn->consumed : 0; + if (left) memmove(conn->in.data, conn->in.data + conn->consumed, left); + conn->in.length = left; + if (conn->in.data) conn->in.data[left] = 0; + conn->consumed = 0; + uv_read_start((uv_stream_t *)&conn->handle, conn_alloc_cb, conn_read_cb); + if (left) conn_try_dispatch(conn); +} + +/* Read straight into the connection's own buffer. It used to allocate 64KB + for every read and copy it in afterwards: a 1MB request meant sixteen + allocations, sixteen frees and a megabyte of copying that the kernel could + have done into the right place to begin with. */ static void conn_alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) { - (void)handle; (void)suggested; - buf->base = malloc(65536); buf->len = buf->base ? 65535 : 0; /* room for a trailing NUL */ + (void)suggested; + ConnState *conn = (ConnState *)handle->data; + size_t room = 65536; + dynbuf_reserve(&conn->in, conn->in.length + room + 1); /* +1 for a trailing NUL */ + if (!conn->in.data) { buf->base = NULL; buf->len = 0; return; } + buf->base = conn->in.data + conn->in.length; + buf->len = conn->in.cap - conn->in.length - 1; } /* arcsx: turn a handler's return value into bytes and write them. Split out @@ -294,9 +418,13 @@ static void conn_deliver(JSContext *ctx, ConnState *conn, JSValue result, DynBuf out = {0}; if (JS_IsException(result)) { JS_FreeValue(ctx, result); - dynbuf_puts(&out, "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n"); + conn->keep_alive = 0; + dynbuf_puts(&out, "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); } else { JSValue mode_value = JS_GetPropertyStr(ctx, result, "mode"); const char *mode = JS_ToCString(ctx, mode_value); + /* An upgrade and an SSE stream both own the connection until the + client goes away; neither can be followed by another request. */ + if (mode && (!strcmp(mode, "websocket") || !strcmp(mode, "sse"))) conn->keep_alive = 0; if (mode && !strcmp(mode, "websocket") && upgrade && ws_key) { websocket_handshake(&out, ws_key); JSValue messages = JS_GetPropertyStr(ctx, result, "wsMessages"); uint32_t length = 0; JSValue size = JS_GetPropertyStr(ctx, messages, "length"); JS_ToUint32(ctx, &length, size); JS_FreeValue(ctx, size); @@ -307,7 +435,10 @@ static void conn_deliver(JSContext *ctx, ConnState *conn, JSValue result, JSValue events = JS_GetPropertyStr(ctx, result, "events"); uint32_t length = 0; JSValue size = JS_GetPropertyStr(ctx, events, "length"); JS_ToUint32(ctx, &length, size); JS_FreeValue(ctx, size); for (uint32_t i = 0; i < length; ++i) { JSValue event = JS_GetPropertyUint32(ctx, events, i); JSValue data = JS_GetPropertyStr(ctx, event, "data"); JSValue type = JS_GetPropertyStr(ctx, event, "event"); JSValue id = JS_GetPropertyStr(ctx, event, "id"); - const char *text = JS_ToCString(ctx, data), *event_name = JS_ToCString(ctx, type), *event_id = JS_ToCString(ctx, id); + const char *text = JS_ToCString(ctx, data), *event_name = (JS_IsUndefined(type) || JS_IsNull(type)) ? NULL : JS_ToCString(ctx, type); + /* An absent id must be left out, not written as the string + "undefined" -- JS_ToCString stringifies undefined. */ + const char *event_id = (JS_IsUndefined(id) || JS_IsNull(id)) ? NULL : JS_ToCString(ctx, id); char full[4096]; int n; if (event_id) n = snprintf(full, sizeof(full), "event: %s\nid: %s\ndata: %s\n\n", event_name ? event_name : "message", event_id, text ? text : ""); else n = snprintf(full, sizeof(full), "%s%s%sdata: %s\n\n", event_name ? "event: " : "", event_name ? event_name : "", event_name ? "\n" : "", text ? text : ""); @@ -435,14 +566,22 @@ static void conn_deliver(JSContext *ctx, ConnState *conn, JSValue result, JS_FreeValue(ctx, hdrs); if (ct_override) content_type = ct_override; - char head[512]; int n = snprintf(head, sizeof(head), "HTTP/1.1 %d %s\r\nContent-Length: %zu\r\nConnection: close\r\nContent-Type: %s\r\n", status, reason(status), body_len, content_type); + /* A HEAD response carries the length the body would have had + and no body at all, which is the one thing a handler cannot + express by returning bytes. */ + JSValue nobody_value = JS_GetPropertyStr(ctx, result, "bodyOmitted"); + int body_omitted = JS_ToBool(ctx, nobody_value); + JS_FreeValue(ctx, nobody_value); + + char head[512]; int n = snprintf(head, sizeof(head), "HTTP/1.1 %d %s\r\nContent-Length: %zu\r\nConnection: %s\r\nContent-Type: %s\r\n", status, reason(status), body_len, conn->keep_alive ? "keep-alive" : "close", content_type); dynbuf_append(&out, head, (size_t)n); if (extra && extra_len) dynbuf_append(&out, extra, extra_len); dynbuf_append(&out, "\r\n", 2); free(extra); if (ct_override) JS_FreeCString(ctx, ct_override); JS_FreeValue(ctx, ct_value); - if (is_binary) { if (body_bytes) dynbuf_append(&out, body_bytes, body_len); } + if (body_omitted) { /* HEAD: the length above, and nothing after it */ } + else if (is_binary) { if (body_bytes) dynbuf_append(&out, body_bytes, body_len); } else if (body_text) dynbuf_append(&out, body_text, body_len); if (borrowed_cv && !JS_IsUndefined(borrow_u8)) { @@ -466,20 +605,98 @@ static void conn_deliver(JSContext *ctx, ConnState *conn, JSValue result, uv_write(write_req, stream, &write_buf, 1, conn_write_cb); } -static void conn_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { - ConnState *conn = (ConnState *)stream->data; - uv_read_stop(stream); - if (nread <= 0) { free(buf->base); uv_close((uv_handle_t *)&conn->handle, conn_close_cb); return; } +/* One complete request, parsed and handed to the handler. `request` is NUL + terminated for the header scanning below, and `length` is what bounds the + body -- a body may legitimately contain a NUL byte, so its length never + comes from strlen. */ +/* The connection's read buffer, once it has been handed to JavaScript. */ +static void sxn_free_read_buffer(JSRuntime *rt, void *opaque, void *ptr) { + (void)rt; (void)opaque; + free(ptr); +} + +/* `body`, materialised only if something reads it. The bytes belong to the + Uint8Array in func_data, so this cannot outlive them. */ +static JSValue conn_body_string(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, + int magic, JSValueConst *func_data) { + (void)this_val; (void)argc; (void)argv; (void)magic; + size_t size = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &size, func_data[0]); + int64_t offset = 0, length = 0; + JS_ToInt64(ctx, &offset, func_data[1]); + JS_ToInt64(ctx, &length, func_data[2]); + if (!bytes || (size_t)(offset + length) > size) return JS_NewString(ctx, ""); + return JS_NewStringLen(ctx, (const char *)bytes + offset, (size_t)length); +} + +/* JSON straight from the bytes, with no string in between: a 1MB request + body would otherwise be copied into a JavaScript string first, and the + parser only wants the bytes. */ +static JSValue sxn_parse_json_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + size_t size = 0; + uint8_t *bytes = argc > 0 ? JS_GetUint8Array(ctx, &size, argv[0]) : NULL; + if (!bytes) return JS_ThrowTypeError(ctx, "expected a Uint8Array"); + int64_t offset = 0, length = (int64_t)size; + if (argc > 1) JS_ToInt64(ctx, &offset, argv[1]); + if (argc > 2) JS_ToInt64(ctx, &length, argv[2]); + if (offset < 0 || length < 0 || (size_t)(offset + length) > size) + return JS_ThrowRangeError(ctx, "body slice out of range"); + return JS_ParseJSON(ctx, (const char *)bytes + offset, (size_t)length, ""); +} + +static void conn_dispatch_request(ConnState *conn, char *request, size_t length) { JSContext *ctx = conn->serve->ctx; - char *request = buf->base; request[nread] = 0; char method[16] = {0}, url[4096] = {0}; sscanf(request, "%15s %4095s", method, url); - char *body = strstr(request, "\r\n\r\n"); body = body ? body + 4 : request + nread; + char *head_end = strstr(request, "\r\n\r\n"); + char *body = head_end ? head_end + 4 : request + length; + size_t body_len = (size_t)((request + length) - body); char *ws_key = header_value(request, "Sec-WebSocket-Key"); char *upgrade = header_value(request, "Upgrade"); JSValue req_obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, req_obj, "method", JS_NewString(ctx, method)); JS_SetPropertyStr(ctx, req_obj, "url", JS_NewString(ctx, url)); - JS_SetPropertyStr(ctx, req_obj, "body", JS_NewString(ctx, body)); + if (body_len == 0) { + JS_SetPropertyStr(ctx, req_obj, "body", JS_NewString(ctx, "")); + } else { + /* Hand the body over as bytes JavaScript owns. When nothing else is + in the buffer -- which is every request that is not pipelined -- + the buffer itself goes, so a megabyte of body is not copied into a + string that the handler may not even read. */ + JSValue bytes; + size_t offset = (size_t)(body - request); + if (conn->in.data == request && conn->in.length == length) { + conn->in.data[length] = 0; /* the parser may look one past the end */ + bytes = JS_NewUint8Array(ctx, (uint8_t *)request, length + 1, + sxn_free_read_buffer, NULL, false); + conn->in.data = NULL; conn->in.length = 0; conn->in.cap = 0; + } else { + /* Another request is already in the buffer behind this one, so + the buffer cannot be handed over. The copy carries a trailing + NUL for the same reason the transferred buffer does: the JSON + parser may look one byte past the body. */ + uint8_t *copy = malloc(length + 1); + if (!copy) { JS_FreeValue(ctx, req_obj); return; } + memcpy(copy, request, length); + copy[length] = 0; + bytes = JS_NewUint8Array(ctx, copy, length + 1, sxn_free_read_buffer, NULL, false); + } + JSValue offset_value = JS_NewInt64(ctx, (int64_t)offset); + JSValue length_value = JS_NewInt64(ctx, (int64_t)body_len); + JS_SetPropertyStr(ctx, req_obj, "bodyBytes", JS_DupValue(ctx, bytes)); + JS_SetPropertyStr(ctx, req_obj, "bodyOffset", JS_DupValue(ctx, offset_value)); + JS_SetPropertyStr(ctx, req_obj, "bodyLength", JS_DupValue(ctx, length_value)); + /* `body` stays a string for node:http, built only if it is read. */ + JSValueConst data[3] = { bytes, offset_value, length_value }; + JSValue getter = JS_NewCFunctionData(ctx, conn_body_string, 0, 0, 3, data); + JSAtom body_atom = JS_NewAtom(ctx, "body"); + JS_DefinePropertyGetSet(ctx, req_obj, body_atom, getter, JS_UNDEFINED, JS_PROP_C_W_E); + JS_FreeAtom(ctx, body_atom); + JS_FreeValue(ctx, bytes); + JS_FreeValue(ctx, offset_value); + JS_FreeValue(ctx, length_value); + } /* Every request header, lowercased, the way Node presents them. Only `upgrade` used to be exposed, so a handler could not read an Authorization, Content-Type or Cookie header at all. */ @@ -542,22 +759,112 @@ static void conn_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf JS_FreeValue(ctx, caught); JS_FreeValue(ctx, chained); JS_FreeValue(ctx, on_ok); JS_FreeValue(ctx, on_err); JS_FreeValue(ctx, data); JS_FreeValue(ctx, result); - free(buf->base); return; } - free(buf->base); conn_deliver(ctx, conn, result, upgrade, ws_key); } +/* HTTP/1.1 keeps a connection open unless the request says otherwise; + HTTP/1.0 closes it unless the request asks for keep-alive. */ +static int request_keeps_alive(const char *head, const char *head_end) { + int one_one = strstr(head, "HTTP/1.1") != NULL && strstr(head, "HTTP/1.1") < head_end; + const char *line = strstr(head, "\r\n"); + while (line && line + 2 < head_end) { + line += 2; + if (!strncasecmp(line, "Connection:", 11)) { + const char *value = line + 11; + while (*value == ' ' || *value == '\t') ++value; + if (!strncasecmp(value, "close", 5)) return 0; + if (!strncasecmp(value, "keep-alive", 10)) return 1; + break; + } + line = strstr(line, "\r\n"); + } + return one_one; +} + +/* Content-Length, or -1 when the head does not carry one. Its own scan + rather than header_value's, which returns an interior pointer and + overwrites the line's CRLF with a NUL -- fine once the request is complete + and about to be parsed, wrong while it is still being read. */ +static long long request_content_length(const char *head, const char *head_end) { + const char *line = strstr(head, "\r\n"); + while (line && line + 2 < head_end) { + line += 2; + if (!strncasecmp(line, "Content-Length:", 15)) { + const char *value = line + 15; + while (*value == ' ' || *value == '\t') ++value; + long long length = strtoll(value, NULL, 10); + return length < 0 ? -1 : length; + } + line = strstr(line, "\r\n"); + } + return -1; +} + +/* Dispatch as soon as a whole request is in the buffer; otherwise wait for + more reads. Called after every read, and again after a response is written + in case the client pipelined the next request behind the last one. */ +static void conn_try_dispatch(ConnState *conn) { + uv_stream_t *stream = (uv_stream_t *)&conn->handle; + if (conn->in.length > SXN_MAX_REQUEST_BYTES) { + uv_read_stop(stream); + conn->keep_alive = 0; + conn_deliver(conn->serve->ctx, conn, JS_EXCEPTION, NULL, NULL); + return; + } + /* Wait for the whole head, then for the whole body it announces. Without + this a request larger than one read -- anything past about 64KB -- was + handed to the handler truncated. */ + char *head_end = strstr(conn->in.data, "\r\n\r\n"); + if (!head_end) return; + long long content_length = request_content_length(conn->in.data, head_end); + size_t head_bytes = (size_t)(head_end + 4 - conn->in.data); + size_t body_bytes = content_length > 0 ? (size_t)content_length : 0; + if (conn->in.length - head_bytes < body_bytes) { + /* Now that the length is known, take the room for it at once. */ + dynbuf_reserve(&conn->in, head_bytes + body_bytes + 1); + return; + } + + uv_read_stop(stream); + conn->keep_alive = request_keeps_alive(conn->in.data, head_end); + conn->consumed = head_bytes + body_bytes; + conn_dispatch_request(conn, conn->in.data, conn->consumed); +} + +static void conn_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { + ConnState *conn = (ConnState *)stream->data; + (void)buf; /* it is a window into conn->in, not a buffer of its own */ + if (nread <= 0) { + uv_read_stop(stream); + conn_shutdown(conn, conn_close_cb); + return; + } + conn->in.length += (size_t)nread; + /* NUL terminated for the header parsing, which is all string work; the + body is bounded by the length instead. */ + conn->in.data[conn->in.length] = 0; + conn_try_dispatch(conn); +} + static void on_connection_cb(uv_stream_t *server_handle, int status) { if (status < 0) return; ServeState *serve = (ServeState *)server_handle->data; ConnState *conn = calloc(1, sizeof(*conn)); conn->serve = serve; uv_tcp_init(sxn_loop(), &conn->handle); conn->handle.data = conn; - if (uv_accept(server_handle, (uv_stream_t *)&conn->handle) == 0) + if (uv_accept(server_handle, (uv_stream_t *)&conn->handle) == 0) { + /* No Nagle: a reply is written in one go and wants to leave now, not + when the kernel has collected enough to be worth a packet. */ + uv_tcp_nodelay(&conn->handle, 1); + conn->next = serve->conns; + if (serve->conns) serve->conns->prev = conn; + serve->conns = conn; uv_read_start((uv_stream_t *)&conn->handle, conn_alloc_cb, conn_read_cb); - else + } else { + conn->serve = NULL; uv_close((uv_handle_t *)&conn->handle, conn_close_cb); + } } /* Stops the listener a serve() call created. The handle is closed @@ -579,6 +886,8 @@ static JSValue js_serve_stop(JSContext *ctx, JSValueConst this_val, void *ptr = JS_GetOpaque(func_data[0], sxn_serverhandle_class_id); if (ptr) { JS_SetOpaque(func_data[0], NULL); + ServeState *serve = (ServeState *)((uv_handle_t *)ptr)->data; + while (serve && serve->conns) conn_shutdown(serve->conns, conn_close_cb); uv_close((uv_handle_t *)ptr, serve_close_cb); } return JS_UNDEFINED; @@ -589,25 +898,105 @@ static JSValue js_serve(JSContext *ctx, JSValueConst this_val, int argc, JSValue if (argc < 2 || !JS_IsFunction(ctx, argv[1])) return JS_ThrowTypeError(ctx, "serve(options, handler) requires a handler"); JSValue port_value = JS_GetPropertyStr(ctx, argv[0], "port"); JS_ToInt32(ctx, &port, port_value); JS_FreeValue(ctx, port_value); - ServeState *serve = malloc(sizeof(*serve)); serve->ctx = ctx; serve->handler = JS_DupValue(ctx, argv[1]); + /* The address to bind. Loopback by default -- a server nobody asked to + expose should not be reachable from the network -- but a real + deployment needs 0.0.0.0, and until this was read the option was + accepted and ignored. `host` is Node's name for it, `hostname` is + Bun's and Deno's; both work. */ + char host[256] = "127.0.0.1"; + { + JSValue h = JS_GetPropertyStr(ctx, argv[0], "hostname"); + if (JS_IsUndefined(h)) { + JS_FreeValue(ctx, h); + h = JS_GetPropertyStr(ctx, argv[0], "host"); + } + if (!JS_IsUndefined(h) && !JS_IsNull(h)) { + const char *str = JS_ToCString(ctx, h); + if (str) { + snprintf(host, sizeof(host), "%s", str); + JS_FreeCString(ctx, str); + } + } + JS_FreeValue(ctx, h); + } + /* SO_REUSEPORT: several processes bind the same port and the kernel + spreads incoming connections across them. This runtime has no threads + and no cluster module, so running N copies of a server is the way to + use N cores, and this is what makes that possible. Linux, the BSDs, + Solaris and AIX only -- macOS's SO_REUSEPORT does not distribute, and + libuv reports ENOTSUP there rather than silently giving the last + binder everything. */ + bool reuse_port = false; + { + JSValue r = JS_GetPropertyStr(ctx, argv[0], "reusePort"); + reuse_port = JS_ToBool(ctx, r); + JS_FreeValue(ctx, r); + } + + ServeState *serve = calloc(1, sizeof(*serve)); serve->ctx = ctx; serve->handler = JS_DupValue(ctx, argv[1]); uv_tcp_t *server = malloc(sizeof(*server)); uv_tcp_init(sxn_loop(), server); server->data = serve; - struct sockaddr_in address; uv_ip4_addr("127.0.0.1", port, &address); - int rc = uv_tcp_bind(server, (const struct sockaddr *)&address, 0); - if (rc == 0) rc = uv_listen((uv_stream_t *)server, 64, on_connection_cb); + struct sockaddr_storage address; + int rc; + /* The one name worth resolving without a DNS lookup, and the one people + actually write. */ + if (!strcmp(host, "localhost")) + snprintf(host, sizeof(host), "127.0.0.1"); + if (uv_ip4_addr(host, port, (struct sockaddr_in *)&address) == 0) + rc = 0; + else if (uv_ip6_addr(host, port, (struct sockaddr_in6 *)&address) == 0) + rc = 0; + else { + free(server); JS_FreeValue(ctx, serve->handler); free(serve); + return JS_ThrowTypeError(ctx, "serve: '%s' is not an IP address to bind", host); + } + if (!reuse_port) { + rc = uv_tcp_bind(server, (const struct sockaddr *)&address, 0); + } else { +#if SXN_UV_HAS_REUSEPORT + rc = uv_tcp_bind(server, (const struct sockaddr *)&address, UV_TCP_REUSEPORT); +#elif defined(SO_REUSEPORT) && defined(__linux__) + /* libuv older than 1.49 has no flag for it, so the socket is made + and configured here and handed over. Linux only: this is where + SO_REUSEPORT distributes connections rather than handing the last + binder everything. */ + int fd = socket(address.ss_family, SOCK_STREAM, 0); + int on = 1; + if (fd < 0) rc = UV_ENOTSUP; + else if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0 + || setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)) != 0 + || bind(fd, (const struct sockaddr *)&address, + address.ss_family == AF_INET6 ? sizeof(struct sockaddr_in6) + : sizeof(struct sockaddr_in)) != 0) { + close(fd); + rc = UV_ENOTSUP; + } else { + rc = uv_tcp_open(server, fd); + if (rc != 0) close(fd); + } +#else + rc = UV_ENOTSUP; +#endif + } + /* 511, the same backlog Node uses: at 64 a burst of concurrent clients got + connection-refused rather than queued. */ + if (rc == 0) rc = uv_listen((uv_stream_t *)server, 511, on_connection_cb); if (rc != 0) { free(server); JS_FreeValue(ctx, serve->handler); free(serve); - return JS_ThrowInternalError(ctx, "listen on %d: %s", port, uv_strerror(rc)); + if (reuse_port && rc == UV_ENOTSUP) + return JS_ThrowInternalError(ctx, "listen on %s:%d: reusePort is not supported on this platform", host, port); + return JS_ThrowInternalError(ctx, "listen on %s:%d: %s", host, port, uv_strerror(rc)); } /* `port: 0` asks the OS to choose a free port, which is the only way to write a test or an example that can't collide with whatever else is already listening. Read back what it chose: reporting the requested 0 left `handle.port` and `handle.url` naming a port nothing can connect to, so the documented `Sxn.serve({ port: 0 }, ...)` was unusable. */ - struct sockaddr_in bound; + struct sockaddr_storage bound; int bound_len = (int)sizeof(bound); if (uv_tcp_getsockname(server, (struct sockaddr *)&bound, &bound_len) == 0) - port = ntohs(bound.sin_port); + port = ntohs(bound.ss_family == AF_INET6 ? ((struct sockaddr_in6 *)&bound)->sin6_port + : ((struct sockaddr_in *)&bound)->sin_port); /* Hand back a handle: without one a server can never be stopped, which makes it impossible to run a server and anything else in one process. */ @@ -618,9 +1007,11 @@ static JSValue js_serve(JSContext *ctx, JSValueConst this_val, int argc, JSValue JS_SetPropertyStr(ctx, handle, "stop", JS_NewCFunctionData(ctx, js_serve_stop, 0, 0, 1, data)); JS_SetPropertyStr(ctx, handle, "port", JS_NewInt32(ctx, port)); + JS_SetPropertyStr(ctx, handle, "hostname", JS_NewString(ctx, host)); { - char u[64]; - snprintf(u, sizeof(u), "http://127.0.0.1:%d", port); + /* A bare IPv6 address needs brackets in a URL. */ + char u[320]; + snprintf(u, sizeof(u), strchr(host, ':') ? "http://[%s]:%d" : "http://%s:%d", host, port); JS_SetPropertyStr(ctx, handle, "url", JS_NewString(ctx, u)); } return handle; @@ -637,10 +1028,12 @@ static JSValue js_serve(JSContext *ctx, JSValueConst this_val, int argc, JSValue /* --- Borrow lock: guards native chunk memory handed to JS zero-copy ----- (Task 4.) Nothing in this codebase constructs an SX20xx diagnostic before this file: include/sxfe.h's SX2001-SX2004 were unused enum - values (only referenced by frontend.c's name-lookup switch), and there - is no compile-time borrow checker anywhere -- this is the first real - runtime use of one of them (SX2002_BORROW_CONFLICT, in + values (only referenced by frontend.c's name-lookup switch) -- this is + the first real runtime use of one of them (SX2002_BORROW_CONFLICT, in sxn_throw_borrow_conflict below), and it stays a runtime notion only. + The parser has since grown one compile-time ownership rule of its own + (SX2003, `&mut` on an immutable binding), but it is a separate notion: + it never reaches this lock, and this lock never consults it. Single-threaded-cooperative, like the rest of this file: fetch_write_cb (libcurl's data callback) only ever runs from inside curl_multi_socket_action, @@ -697,7 +1090,7 @@ typedef struct FetchState { JSContext *ctx; CURL *easy; struct curl_slist *req_headers; - char *request_body; + char *request_body; size_t request_body_len; char *url; long status; @@ -1267,10 +1660,20 @@ static JSValue js_sxn_fetch_raw(JSContext *ctx, JSValueConst this_val, int argc, } } - char *request_body = NULL; + /* argv[4], when given, is the redirect mode. */ + long follow = 1; + if (argc > 4 && JS_IsString(argv[4])) { + const char *mode = JS_ToCString(ctx, argv[4]); + if (mode) { follow = strcmp(mode, "follow") == 0; JS_FreeCString(ctx, mode); } + } + char *request_body = NULL; size_t request_body_len = 0; if (argc > 3 && !JS_IsUndefined(argv[3]) && !JS_IsNull(argv[3])) { - const char *b = JS_ToCString(ctx, argv[3]); - if (b) request_body = strdup(b); + /* Counted, not NUL terminated: a request body may contain a 0x00 + byte, and strlen would send everything before it and drop the + rest. */ + size_t len = 0; + const char *b = JS_ToCStringLen(ctx, &len, argv[3]); + if (b) { request_body = malloc(len + 1); memcpy(request_body, b, len); request_body[len] = 0; request_body_len = len; } JS_FreeCString(ctx, b); } @@ -1285,13 +1688,16 @@ static JSValue js_sxn_fetch_raw(JSContext *ctx, JSValueConst this_val, int argc, fs->ctx = ctx; fs->easy = easy; fs->refcount = 1; fs->url = strdup(url); fs->req_headers = headers; - fs->request_body = request_body; + fs->request_body = request_body; fs->request_body_len = request_body_len; fs->header_pairs = JS_UNDEFINED; fs->pending_read_resolve = JS_UNDEFINED; fs->pending_read_reject = JS_UNDEFINED; curl_easy_setopt(easy, CURLOPT_URL, url); - curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(easy, CURLOPT_USERAGENT, "sxn/0.0.1"); + /* Fetch's `redirect` option: "follow" is the default, "manual" hands the + 3xx back as it arrived, and "error" is rejected by the JS wrapper -- + both need the transfer to stop at the first response. */ + curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, follow ? 1L : 0L); + curl_easy_setopt(easy, CURLOPT_USERAGENT, "sxn/0.0.2"); curl_easy_setopt(easy, CURLOPT_PRIVATE, fs); curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, fetch_header_cb); curl_easy_setopt(easy, CURLOPT_HEADERDATA, fs); @@ -1299,13 +1705,17 @@ static JSValue js_sxn_fetch_raw(JSContext *ctx, JSValueConst this_val, int argc, curl_easy_setopt(easy, CURLOPT_WRITEDATA, fs); if (headers) curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers); if (method) curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, method); + /* A HEAD response carries the length of a body it does not send, so the + transfer has to be told there is nothing to wait for. Without this a + fetch of a HEAD hung until the server closed the connection. */ + if (method && !sxn_strcasecmp(method, "HEAD")) curl_easy_setopt(easy, CURLOPT_NOBODY, 1L); if (request_body) { /* POSTFIELDSIZE must be set BEFORE COPYPOSTFIELDS: libcurl reads the size at the moment the fields are copied. Setting it afterwards left curl expecting an upload it had no read callback for, so it connected and then sent nothing at all -- every POST, PUT and PATCH hung until it timed out. */ - curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, (long)strlen(request_body)); + curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, (long)request_body_len); curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, request_body); } JS_FreeCString(ctx, url); JS_FreeCString(ctx, method); @@ -1430,6 +1840,62 @@ static JSValue sxn_random_bytes(JSContext *ctx, JSValueConst this_val, int argc, return result; } + +/* HMAC, from the library that already does the digests. It was built here in + JavaScript out of two padded key buffers and three digest calls, with a + Uint8Array allocated per update. */ + +/* Buffer#compare: Node orders by unsigned byte value, then by length. A + JavaScript loop compared a byte at a time; memcmp compares a word at a + time and is what the C library is for. */ +static JSValue sxn_bytes_compare(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + size_t a_len = 0, b_len = 0; + uint8_t *a = argc > 0 ? JS_GetUint8Array(ctx, &a_len, argv[0]) : NULL; + uint8_t *b = argc > 1 ? JS_GetUint8Array(ctx, &b_len, argv[1]) : NULL; + if (!a || !b) return JS_ThrowTypeError(ctx, "compare expects two Uint8Arrays"); + size_t n = a_len < b_len ? a_len : b_len; + int rc = n ? memcmp(a, b, n) : 0; + if (rc == 0) rc = a_len == b_len ? 0 : (a_len < b_len ? -1 : 1); + return JS_NewInt32(ctx, rc < 0 ? -1 : (rc > 0 ? 1 : 0)); +} + +static JSValue sxn_hmac(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *algo = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!algo) return JS_EXCEPTION; + size_t key_len = 0, data_len = 0; + uint8_t *key = argc > 1 ? JS_GetUint8Array(ctx, &key_len, argv[1]) : NULL; + uint8_t *data = argc > 2 ? JS_GetUint8Array(ctx, &data_len, argv[2]) : NULL; + if (!key || !data) { JS_FreeCString(ctx, algo); return JS_ThrowTypeError(ctx, "hmac expects two Uint8Arrays"); } + char normalized[32]; size_t j = 0; + for (size_t i = 0; algo[i] && j + 1 < sizeof(normalized); i++) + if (algo[i] != '-') normalized[j++] = (char)tolower((unsigned char)algo[i]); + normalized[j] = 0; + const EVP_MD *md = EVP_get_digestbyname(normalized); + JS_FreeCString(ctx, algo); + if (!md) return JS_ThrowTypeError(ctx, "unsupported digest algorithm"); + uint8_t out[EVP_MAX_MD_SIZE]; + unsigned int out_len = 0; + /* An empty key is legal and HMAC() wants a non-NULL pointer for it. */ + if (!HMAC(md, key_len ? (const void *)key : (const void *)"", (int)key_len, + data, data_len, out, &out_len)) + return JS_ThrowInternalError(ctx, "hmac failed"); + return JS_NewUint8ArrayCopy(ctx, out, out_len); +} + +/* Comparison that takes the same time whether the bytes match or not, which + is the whole point of it and is not something JavaScript can promise. */ +static JSValue sxn_timing_safe_equal(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + size_t a_len = 0, b_len = 0; + uint8_t *a = argc > 0 ? JS_GetUint8Array(ctx, &a_len, argv[0]) : NULL; + uint8_t *b = argc > 1 ? JS_GetUint8Array(ctx, &b_len, argv[1]) : NULL; + if (!a || !b) return JS_ThrowTypeError(ctx, "timingSafeEqual expects two Uint8Arrays"); + if (a_len != b_len) return JS_ThrowRangeError(ctx, "input length mismatch"); + return JS_NewBool(ctx, CRYPTO_memcmp(a, b, a_len) == 0); +} + static JSValue sxn_digest(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; const char *algo = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; @@ -1832,6 +2298,655 @@ static JSValue sxn_file(JSContext *ctx, JSValueConst this_val, int argc, JSValue JS_FreeCString(ctx, path); return file; } +/* The only way to reach fd 2 from JS: console.error/warn and + process.stderr are built on this, so a diagnostic does not land in the + program's own stdout. */ +/* node:os, from libuv rather than from guesses. The JS layer used to answer + "localhost" for the hostname and 0 for the memory sizes, which is worse + than not answering: a program cannot tell a stub from the truth. */ +/* node:fs's stat, from libuv. The JS layer had no way to ask a file's size + or kind at all, so `stat` and everything built on it -- a static file + server, a build step that skips unchanged files -- was out of reach. */ +static JSValue sxn_stat(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *path = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!path) return JS_ThrowTypeError(ctx, "stat(path) requires a path"); + bool follow = argc < 2 || JS_ToBool(ctx, argv[1]); + uv_fs_t req; + int rc = follow ? uv_fs_stat(NULL, &req, path, NULL) : uv_fs_lstat(NULL, &req, path, NULL); + if (rc != 0) { + JSValue error = JS_ThrowInternalError(ctx, "%s: %s", uv_strerror(rc), path); + JSValue exception = JS_GetException(ctx); + JS_SetPropertyStr(ctx, exception, "code", JS_NewString(ctx, uv_err_name(rc))); + JS_SetPropertyStr(ctx, exception, "path", JS_NewString(ctx, path)); + JS_Throw(ctx, exception); + JS_FreeCString(ctx, path); + uv_fs_req_cleanup(&req); + return error; + } + const uv_stat_t *st = &req.statbuf; + /* The caller passes Stats.prototype, so the object comes back already + being a Stats -- node:fs used to copy every field of this onto a fresh + one with a for-in loop. */ + JSValue out = (argc > 2 && JS_IsObject(argv[2])) ? JS_NewObjectProto(ctx, argv[2]) : JS_NewObject(ctx); + JS_SetPropertyStr(ctx, out, "dev", JS_NewFloat64(ctx, (double)st->st_dev)); + JS_SetPropertyStr(ctx, out, "ino", JS_NewFloat64(ctx, (double)st->st_ino)); + JS_SetPropertyStr(ctx, out, "mode", JS_NewInt64(ctx, (int64_t)st->st_mode)); + JS_SetPropertyStr(ctx, out, "nlink", JS_NewFloat64(ctx, (double)st->st_nlink)); + JS_SetPropertyStr(ctx, out, "uid", JS_NewInt64(ctx, (int64_t)st->st_uid)); + JS_SetPropertyStr(ctx, out, "gid", JS_NewInt64(ctx, (int64_t)st->st_gid)); + JS_SetPropertyStr(ctx, out, "size", JS_NewFloat64(ctx, (double)st->st_size)); + JS_SetPropertyStr(ctx, out, "blksize", JS_NewFloat64(ctx, (double)st->st_blksize)); + JS_SetPropertyStr(ctx, out, "blocks", JS_NewFloat64(ctx, (double)st->st_blocks)); + JS_SetPropertyStr(ctx, out, "atimeMs", JS_NewFloat64(ctx, st->st_atim.tv_sec * 1000.0 + st->st_atim.tv_nsec / 1e6)); + JS_SetPropertyStr(ctx, out, "mtimeMs", JS_NewFloat64(ctx, st->st_mtim.tv_sec * 1000.0 + st->st_mtim.tv_nsec / 1e6)); + JS_SetPropertyStr(ctx, out, "ctimeMs", JS_NewFloat64(ctx, st->st_ctim.tv_sec * 1000.0 + st->st_ctim.tv_nsec / 1e6)); + JS_SetPropertyStr(ctx, out, "birthtimeMs", JS_NewFloat64(ctx, st->st_birthtim.tv_sec * 1000.0 + st->st_birthtim.tv_nsec / 1e6)); + /* Node's Stats carries the same four times over again as Dates. */ + JSValue global = JS_GetGlobalObject(ctx); + JSValue date_class = JS_GetPropertyStr(ctx, global, "Date"); + JS_FreeValue(ctx, global); + static const char *time_names[4] = { "atime", "mtime", "ctime", "birthtime" }; + double times[4] = { + st->st_atim.tv_sec * 1000.0 + st->st_atim.tv_nsec / 1e6, + st->st_mtim.tv_sec * 1000.0 + st->st_mtim.tv_nsec / 1e6, + st->st_ctim.tv_sec * 1000.0 + st->st_ctim.tv_nsec / 1e6, + st->st_birthtim.tv_sec * 1000.0 + st->st_birthtim.tv_nsec / 1e6, + }; + for (int i = 0; i < 4; i++) { + JSValue ms = JS_NewFloat64(ctx, times[i]); + JSValueConst args[1] = { ms }; + JS_SetPropertyStr(ctx, out, time_names[i], JS_CallConstructor(ctx, date_class, 1, args)); + JS_FreeValue(ctx, ms); + } + JS_FreeValue(ctx, date_class); + JS_FreeCString(ctx, path); + uv_fs_req_cleanup(&req); + return out; +} + +/* fs's open/access flags, taken from this platform's own headers rather than + written down: O_CREAT alone is 0x200 on macOS and 0x40 on Linux. */ +static JSValue js_fs_constants(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + JSValue out = JS_NewObject(ctx); +#define SXN_CONST(name, value) JS_SetPropertyStr(ctx, out, name, JS_NewInt32(ctx, (int32_t)(value))) + SXN_CONST("O_RDONLY", O_RDONLY); + SXN_CONST("O_WRONLY", O_WRONLY); + SXN_CONST("O_RDWR", O_RDWR); + SXN_CONST("O_CREAT", O_CREAT); + SXN_CONST("O_EXCL", O_EXCL); + SXN_CONST("O_TRUNC", O_TRUNC); + SXN_CONST("O_APPEND", O_APPEND); + SXN_CONST("F_OK", 0); + SXN_CONST("R_OK", 4); + SXN_CONST("W_OK", 2); + SXN_CONST("X_OK", 1); + SXN_CONST("S_IFMT", 0170000); + SXN_CONST("S_IFREG", 0100000); + SXN_CONST("S_IFDIR", 0040000); + SXN_CONST("S_IFLNK", 0120000); + SXN_CONST("COPYFILE_EXCL", 1); +#undef SXN_CONST + return out; +} + +/* ---------------- UDP (node:dgram) ---------------- + A uv_udp_t on the same loop everything else runs on, with the socket's + callbacks handed straight to JavaScript. node_compat.js puts the + EventEmitter shape around it. */ +static JSClassID sxn_udp_class_id; + +typedef struct { + uv_udp_t handle; + JSContext *ctx; + JSValue on_message; + bool open, reading; +} SxnUdp; + +static void sxn_udp_closed(uv_handle_t *handle) { free(handle->data); } + +static void sxn_udp_finalizer(JSRuntime *rt, JSValue val) { + SxnUdp *u = JS_GetOpaque(val, sxn_udp_class_id); + if (!u) return; + JS_FreeValueRT(rt, u->on_message); + u->on_message = JS_UNDEFINED; + if (u->open) { + u->open = false; + u->handle.data = u; + uv_close((uv_handle_t *)&u->handle, sxn_udp_closed); + } else { + free(u); + } +} + +static JSClassDef sxn_udp_class_def = { + .class_name = "UdpSocket", + .finalizer = sxn_udp_finalizer, +}; + +static void sxn_udp_alloc(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) { + (void)handle; + buf->base = malloc(suggested); + buf->len = buf->base ? suggested : 0; +} + +static void sxn_udp_recv(uv_udp_t *handle, ssize_t nread, const uv_buf_t *buf, + const struct sockaddr *addr, unsigned flags) { + (void)flags; + SxnUdp *u = handle->data; + if (nread > 0 && addr && u && JS_IsFunction(u->ctx, u->on_message)) { + JSContext *ctx = u->ctx; + char text[INET6_ADDRSTRLEN] = {0}; + int port = 0; + if (addr->sa_family == AF_INET6) { + uv_ip6_name((struct sockaddr_in6 *)addr, text, sizeof(text)); + port = ntohs(((struct sockaddr_in6 *)addr)->sin6_port); + } else { + uv_ip4_name((struct sockaddr_in *)addr, text, sizeof(text)); + port = ntohs(((struct sockaddr_in *)addr)->sin_port); + } + JSValue args[3]; + args[0] = JS_NewUint8ArrayCopy(ctx, (const uint8_t *)buf->base, (size_t)nread); + args[1] = JS_NewString(ctx, text); + args[2] = JS_NewInt32(ctx, port); + JSValue r = JS_Call(ctx, u->on_message, JS_UNDEFINED, 3, (JSValueConst *)args); + if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx)); + JS_FreeValue(ctx, r); + for (int i = 0; i < 3; i++) JS_FreeValue(ctx, args[i]); + } + free(buf->base); +} + +/* __sxnUdpOpen(ipv6, onMessage) */ +static JSValue js_udp_open(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + SxnUdp *u = calloc(1, sizeof(*u)); + if (!u) return JS_ThrowOutOfMemory(ctx); + u->ctx = ctx; + u->on_message = argc > 1 ? JS_DupValue(ctx, argv[1]) : JS_UNDEFINED; + if (uv_udp_init(sxn_loop(), &u->handle) != 0) { + JS_FreeValue(ctx, u->on_message); + free(u); + return JS_ThrowInternalError(ctx, "udp init failed"); + } + u->handle.data = u; + u->open = true; + JS_NewClassID(JS_GetRuntime(ctx), &sxn_udp_class_id); + JS_NewClass(JS_GetRuntime(ctx), sxn_udp_class_id, &sxn_udp_class_def); + JSValue obj = JS_NewObjectClass(ctx, sxn_udp_class_id); + if (JS_IsException(obj)) return obj; + JS_SetOpaque(obj, u); + return obj; +} + +static SxnUdp *sxn_udp_of(JSContext *ctx, JSValueConst val) { + SxnUdp *u = JS_GetOpaque(val, sxn_udp_class_id); + return (u && u->open) ? u : NULL; +} + +/* __sxnUdpBind(socket, port, address) -> the port actually bound */ +static JSValue js_udp_bind(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + SxnUdp *u = argc > 0 ? sxn_udp_of(ctx, argv[0]) : NULL; + if (!u) return JS_ThrowTypeError(ctx, "not an open udp socket"); + int32_t port = 0; + if (argc > 1) JS_ToInt32(ctx, &port, argv[1]); + const char *address = argc > 2 && JS_IsString(argv[2]) ? JS_ToCString(ctx, argv[2]) : NULL; + struct sockaddr_storage addr; + int rc = uv_ip4_addr(address ? address : "0.0.0.0", port, (struct sockaddr_in *)&addr); + if (rc != 0) rc = uv_ip6_addr(address ? address : "::", port, (struct sockaddr_in6 *)&addr); + if (address) JS_FreeCString(ctx, address); + if (rc == 0) rc = uv_udp_bind(&u->handle, (const struct sockaddr *)&addr, 0); + if (rc != 0) return JS_ThrowInternalError(ctx, "udp bind failed: %s", uv_strerror(rc)); + if (!u->reading) { + rc = uv_udp_recv_start(&u->handle, sxn_udp_alloc, sxn_udp_recv); + if (rc != 0) return JS_ThrowInternalError(ctx, "udp listen failed: %s", uv_strerror(rc)); + u->reading = true; + } + struct sockaddr_storage bound; + int len = sizeof(bound); + if (uv_udp_getsockname(&u->handle, (struct sockaddr *)&bound, &len) != 0) + return JS_NewInt32(ctx, port); + return JS_NewInt32(ctx, bound.ss_family == AF_INET6 + ? ntohs(((struct sockaddr_in6 *)&bound)->sin6_port) + : ntohs(((struct sockaddr_in *)&bound)->sin_port)); +} + +static void sxn_udp_sent(uv_udp_send_t *req, int status) { (void)status; free(req); } + +/* __sxnUdpSend(socket, bytes, port, address) */ +static JSValue js_udp_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + SxnUdp *u = argc > 0 ? sxn_udp_of(ctx, argv[0]) : NULL; + if (!u) return JS_ThrowTypeError(ctx, "not an open udp socket"); + size_t len = 0; + uint8_t *bytes = argc > 1 ? JS_GetUint8Array(ctx, &len, argv[1]) : NULL; + if (!bytes) return JS_ThrowTypeError(ctx, "udp send expects bytes"); + int32_t port = 0; + if (argc > 2) JS_ToInt32(ctx, &port, argv[2]); + const char *address = argc > 3 && JS_IsString(argv[3]) ? JS_ToCString(ctx, argv[3]) : NULL; + struct sockaddr_storage addr; + int rc = uv_ip4_addr(address ? address : "127.0.0.1", port, (struct sockaddr_in *)&addr); + if (rc != 0) rc = uv_ip6_addr(address ? address : "::1", port, (struct sockaddr_in6 *)&addr); + if (address) JS_FreeCString(ctx, address); + if (rc != 0) return JS_ThrowTypeError(ctx, "udp send: bad address"); + /* uv_udp_send copies nothing, so the write has to finish before the + caller's bytes can move: uv_udp_try_send does it inline, and the + queued path gets its own copy. */ + uv_buf_t buf = uv_buf_init((char *)bytes, (unsigned int)len); + rc = uv_udp_try_send(&u->handle, &buf, 1, (const struct sockaddr *)&addr); + if (rc >= 0) return JS_NewInt32(ctx, rc); + uv_udp_send_t *req = malloc(sizeof(*req) + len); + if (!req) return JS_ThrowOutOfMemory(ctx); + char *copy = (char *)(req + 1); + memcpy(copy, bytes, len); + uv_buf_t queued = uv_buf_init(copy, (unsigned int)len); + rc = uv_udp_send(req, &u->handle, &queued, 1, (const struct sockaddr *)&addr, sxn_udp_sent); + if (rc != 0) { free(req); return JS_ThrowInternalError(ctx, "udp send failed: %s", uv_strerror(rc)); } + return JS_NewInt32(ctx, (int32_t)len); +} + +/* __sxnUdpClose(socket) */ +static JSValue js_udp_close(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + SxnUdp *u = argc > 0 ? JS_GetOpaque(argv[0], sxn_udp_class_id) : NULL; + if (!u || !u->open) return JS_UNDEFINED; + JS_FreeValue(ctx, u->on_message); + u->on_message = JS_UNDEFINED; + u->open = false; + u->handle.data = u; + uv_close((uv_handle_t *)&u->handle, sxn_udp_closed); + JS_SetOpaque(argv[0], NULL); + return JS_UNDEFINED; +} + +/* ---------------- process spawning (node:child_process) ---------------- + One child, run to completion on a loop of its own so that nothing else + queued on the default loop runs re-entrantly while we wait. That makes + this the synchronous form; the asynchronous forms in node_compat.js are + built on it, and say so. */ +typedef struct { char *data; size_t len, cap; } SxnGrow; + +static void sxn_grow_push(SxnGrow *b, const char *p, size_t n) { + if (b->len + n > b->cap) { + size_t want = b->cap ? b->cap * 2 : 8192; + while (want < b->len + n) want *= 2; + char *next = realloc(b->data, want); + if (!next) return; + b->data = next; b->cap = want; + } + memcpy(b->data + b->len, p, n); + b->len += n; +} + +static void sxn_spawn_alloc(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) { + (void)handle; + buf->base = malloc(suggested); + buf->len = buf->base ? suggested : 0; +} + +static void sxn_spawn_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { + if (nread > 0) sxn_grow_push((SxnGrow *)stream->data, buf->base, (size_t)nread); + else if (nread < 0) uv_close((uv_handle_t *)stream, NULL); + free(buf->base); +} + +typedef struct { int64_t status; int signal; } SxnExit; + +static void sxn_spawn_exit(uv_process_t *proc, int64_t status, int signal) { + SxnExit *out = proc->data; + out->status = status; out->signal = signal; + uv_close((uv_handle_t *)proc, NULL); +} + +/* A loop that is closed while a handle is still registered leaves libuv's + own child-process bookkeeping behind it, and the next uv_spawn walks into + what is left: on Linux that is a segfault, not a leak. So everything still + open is closed, the loop is run until those closes complete, and only then + is it closed. */ +static void sxn_spawn_close_walk(uv_handle_t *handle, void *arg) { + (void)arg; + if (!uv_is_closing(handle)) uv_close(handle, NULL); +} + +static void sxn_spawn_teardown(uv_loop_t *loop) { + uv_walk(loop, sxn_spawn_close_walk, NULL); + while (uv_run(loop, UV_RUN_DEFAULT) != 0) { } + uv_loop_close(loop); +} + +static void sxn_spawn_written(uv_write_t *req, int status) { + (void)status; + uv_close((uv_handle_t *)req->handle, NULL); + free(req); +} + +/* __sxnSpawnSync(file, args, options) -> { pid, status, signal, stdout, stderr, error } */ +static JSValue js_spawn_sync(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_ThrowTypeError(ctx, "spawn needs a command"); + const char *file = JS_ToCString(ctx, argv[0]); + if (!file) return JS_EXCEPTION; + + uint32_t nargs = 0; + if (argc > 1 && JS_IsArray(argv[1])) { + JSValue len = JS_GetPropertyStr(ctx, argv[1], "length"); + JS_ToUint32(ctx, &nargs, len); + JS_FreeValue(ctx, len); + } + char **args = calloc(nargs + 2, sizeof(char *)); + args[0] = (char *)file; + for (uint32_t i = 0; i < nargs; i++) { + JSValue item = JS_GetPropertyUint32(ctx, argv[1], i); + const char *s = JS_ToCString(ctx, item); + args[i + 1] = s ? strdup(s) : strdup(""); + if (s) JS_FreeCString(ctx, s); + JS_FreeValue(ctx, item); + } + + char *cwd = NULL, **env = NULL; + const char *input = NULL; + size_t input_len = 0; + if (argc > 2 && JS_IsObject(argv[2])) { + JSValue v = JS_GetPropertyStr(ctx, argv[2], "cwd"); + if (JS_IsString(v)) { const char *s = JS_ToCString(ctx, v); if (s) { cwd = strdup(s); JS_FreeCString(ctx, s); } } + JS_FreeValue(ctx, v); + v = JS_GetPropertyStr(ctx, argv[2], "input"); + if (JS_IsString(v)) input = JS_ToCStringLen(ctx, &input_len, v); + JS_FreeValue(ctx, v); + v = JS_GetPropertyStr(ctx, argv[2], "env"); + if (JS_IsObject(v)) { + JSPropertyEnum *props = NULL; uint32_t count = 0; + if (JS_GetOwnPropertyNames(ctx, &props, &count, v, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { + env = calloc(count + 1, sizeof(char *)); + uint32_t written = 0; + for (uint32_t i = 0; i < count; i++) { + JSValue val = JS_GetProperty(ctx, v, props[i].atom); + const char *key = JS_AtomToCString(ctx, props[i].atom); + const char *sval = JS_ToCString(ctx, val); + if (key && sval) { + size_t n = strlen(key) + strlen(sval) + 2; + char *entry = malloc(n); + snprintf(entry, n, "%s=%s", key, sval); + env[written++] = entry; + } + if (key) JS_FreeCString(ctx, key); + if (sval) JS_FreeCString(ctx, sval); + JS_FreeValue(ctx, val); + JS_FreeAtom(ctx, props[i].atom); + } + js_free(ctx, props); + } + } + JS_FreeValue(ctx, v); + } + + uv_loop_t loop; + uv_loop_init(&loop); + SxnGrow out = {0}, err = {0}; + uv_pipe_t in_pipe, out_pipe, err_pipe; + uv_pipe_init(&loop, &in_pipe, 0); + uv_pipe_init(&loop, &out_pipe, 0); + uv_pipe_init(&loop, &err_pipe, 0); + out_pipe.data = &out; + err_pipe.data = &err; + + uv_stdio_container_t stdio[3]; + stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + stdio[0].data.stream = (uv_stream_t *)&in_pipe; + stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + stdio[1].data.stream = (uv_stream_t *)&out_pipe; + stdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + stdio[2].data.stream = (uv_stream_t *)&err_pipe; + + SxnExit exit_state = { -1, 0 }; + uv_process_t proc; + proc.data = &exit_state; + uv_process_options_t options; + memset(&options, 0, sizeof(options)); + options.file = file; + options.args = args; + options.cwd = cwd; + options.env = env; + options.stdio = stdio; + options.stdio_count = 3; + options.exit_cb = sxn_spawn_exit; + + int rc = uv_spawn(&loop, &proc, &options); + JSValue result = JS_NewObject(ctx); + if (rc != 0) { + uv_close((uv_handle_t *)&in_pipe, NULL); + uv_close((uv_handle_t *)&out_pipe, NULL); + uv_close((uv_handle_t *)&err_pipe, NULL); + sxn_spawn_teardown(&loop); + JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, uv_strerror(rc))); + JS_SetPropertyStr(ctx, result, "errno", JS_NewString(ctx, uv_err_name(rc))); + JS_SetPropertyStr(ctx, result, "status", JS_NULL); + } else { + uv_read_start((uv_stream_t *)&out_pipe, sxn_spawn_alloc, sxn_spawn_read); + uv_read_start((uv_stream_t *)&err_pipe, sxn_spawn_alloc, sxn_spawn_read); + if (input) { + uv_write_t *req = calloc(1, sizeof(*req)); + uv_buf_t buf = uv_buf_init((char *)input, (unsigned int)input_len); + if (!req || uv_write(req, (uv_stream_t *)&in_pipe, &buf, 1, sxn_spawn_written) != 0) { + free(req); + uv_close((uv_handle_t *)&in_pipe, NULL); + } + } else { + uv_close((uv_handle_t *)&in_pipe, NULL); + } + uv_run(&loop, UV_RUN_DEFAULT); + sxn_spawn_teardown(&loop); + JS_SetPropertyStr(ctx, result, "pid", JS_NewInt32(ctx, proc.pid)); + JS_SetPropertyStr(ctx, result, "status", + exit_state.signal ? JS_NULL : JS_NewInt64(ctx, exit_state.status)); + JS_SetPropertyStr(ctx, result, "signal", + exit_state.signal ? JS_NewInt32(ctx, exit_state.signal) : JS_NULL); + } + JS_SetPropertyStr(ctx, result, "stdout", + JS_NewUint8ArrayCopy(ctx, (const uint8_t *)(out.data ? out.data : ""), out.len)); + JS_SetPropertyStr(ctx, result, "stderr", + JS_NewUint8ArrayCopy(ctx, (const uint8_t *)(err.data ? err.data : ""), err.len)); + + if (input) JS_FreeCString(ctx, input); + free(out.data); free(err.data); + for (uint32_t i = 0; i < nargs; i++) free(args[i + 1]); + free(args); + free(cwd); + if (env) { for (char **e = env; *e; e++) free(*e); free(env); } + JS_FreeCString(ctx, file); + return result; +} + +/* __sxnDnsLookup(hostname, family) -> [{ address, family }, ...] + uv_getaddrinfo with no callback resolves on this thread, which is what + node:dns's callback forms then hand back on a later tick. */ +static JSValue js_dns_lookup(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_ThrowTypeError(ctx, "lookup needs a hostname"); + const char *host = JS_ToCString(ctx, argv[0]); + if (!host) return JS_EXCEPTION; + int32_t family = 0; + if (argc > 1) JS_ToInt32(ctx, &family, argv[1]); + + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = family == 4 ? AF_INET : family == 6 ? AF_INET6 : AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + uv_getaddrinfo_t req; + int rc = uv_getaddrinfo(uv_default_loop(), &req, NULL, host, NULL, &hints); + JS_FreeCString(ctx, host); + if (rc != 0) { + JSValue error = JS_NewError(ctx); + JS_SetPropertyStr(ctx, error, "message", JS_NewString(ctx, uv_strerror(rc))); + JS_SetPropertyStr(ctx, error, "code", JS_NewString(ctx, rc == UV_EAI_NONAME ? "ENOTFOUND" : uv_err_name(rc))); + return JS_Throw(ctx, error); + } + JSValue list = JS_NewArray(ctx); + uint32_t n = 0; + for (struct addrinfo *ai = req.addrinfo; ai; ai = ai->ai_next) { + char text[INET6_ADDRSTRLEN] = {0}; + int is6 = ai->ai_family == AF_INET6; + if (is6) uv_ip6_name((struct sockaddr_in6 *)ai->ai_addr, text, sizeof(text)); + else if (ai->ai_family == AF_INET) uv_ip4_name((struct sockaddr_in *)ai->ai_addr, text, sizeof(text)); + else continue; + JSValue entry = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, entry, "address", JS_NewString(ctx, text)); + JS_SetPropertyStr(ctx, entry, "family", JS_NewInt32(ctx, is6 ? 6 : 4)); + JS_SetPropertyUint32(ctx, list, n++, entry); + } + uv_freeaddrinfo(req.addrinfo); + return list; +} + +static JSValue sxn_os_hostname(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + char name[UV_MAXHOSTNAMESIZE]; + size_t size = sizeof(name); + if (uv_os_gethostname(name, &size) != 0) return JS_NewString(ctx, "localhost"); + return JS_NewString(ctx, name); +} + +static JSValue sxn_os_dir(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + (void)this_val; (void)argc; (void)argv; + char path[4096]; + size_t size = sizeof(path); + int rc = magic == 0 ? uv_os_homedir(path, &size) : uv_os_tmpdir(path, &size); + if (rc != 0) return JS_NewString(ctx, magic == 0 ? "/" : "/tmp"); + return JS_NewString(ctx, path); +} + +static JSValue sxn_os_uname(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + uv_utsname_t name; + JSValue out = JS_NewObject(ctx); + if (uv_os_uname(&name) != 0) return out; + JS_SetPropertyStr(ctx, out, "sysname", JS_NewString(ctx, name.sysname)); + JS_SetPropertyStr(ctx, out, "release", JS_NewString(ctx, name.release)); + JS_SetPropertyStr(ctx, out, "version", JS_NewString(ctx, name.version)); + JS_SetPropertyStr(ctx, out, "machine", JS_NewString(ctx, name.machine)); + return out; +} + +static JSValue sxn_os_numbers(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + double avg[3] = {0, 0, 0}; + uv_loadavg(avg); + JSValue out = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, out, "totalmem", JS_NewFloat64(ctx, (double)uv_get_total_memory())); + JS_SetPropertyStr(ctx, out, "freemem", JS_NewFloat64(ctx, (double)uv_get_free_memory())); + JS_SetPropertyStr(ctx, out, "uptime", JS_NewFloat64(ctx, ({ double up = 0; uv_uptime(&up); up; }))); + JS_SetPropertyStr(ctx, out, "parallelism", JS_NewInt32(ctx, (int32_t)uv_available_parallelism())); + JSValue load = JS_NewArray(ctx); + for (int i = 0; i < 3; i++) JS_SetPropertyUint32(ctx, load, i, JS_NewFloat64(ctx, avg[i])); + JS_SetPropertyStr(ctx, out, "loadavg", load); + return out; +} + +static JSValue sxn_os_cpus(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + uv_cpu_info_t *info = NULL; + int count = 0; + JSValue out = JS_NewArray(ctx); + if (uv_cpu_info(&info, &count) != 0) return out; + for (int i = 0; i < count; i++) { + JSValue cpu = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, cpu, "model", JS_NewString(ctx, info[i].model)); + JS_SetPropertyStr(ctx, cpu, "speed", JS_NewInt32(ctx, info[i].speed)); + JSValue times = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, times, "user", JS_NewFloat64(ctx, (double)info[i].cpu_times.user)); + JS_SetPropertyStr(ctx, times, "nice", JS_NewFloat64(ctx, (double)info[i].cpu_times.nice)); + JS_SetPropertyStr(ctx, times, "sys", JS_NewFloat64(ctx, (double)info[i].cpu_times.sys)); + JS_SetPropertyStr(ctx, times, "idle", JS_NewFloat64(ctx, (double)info[i].cpu_times.idle)); + JS_SetPropertyStr(ctx, times, "irq", JS_NewFloat64(ctx, (double)info[i].cpu_times.irq)); + JS_SetPropertyStr(ctx, cpu, "times", times); + JS_SetPropertyUint32(ctx, out, (uint32_t)i, cpu); + } + uv_free_cpu_info(info, count); + return out; +} + +/* How many leading one-bits a netmask has, which is what a CIDR suffix is. */ +static int sxn_mask_prefix(const uint8_t *bytes, int len) { + int bits = 0; + for (int i = 0; i < len; i++) { + if (bytes[i] == 0xff) { bits += 8; continue; } + uint8_t b = bytes[i]; + while (b & 0x80) { bits++; b <<= 1; } + break; + } + return bits; +} + +static JSValue sxn_os_interfaces(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + uv_interface_address_t *addresses = NULL; + int count = 0; + JSValue out = JS_NewObject(ctx); + if (uv_interface_addresses(&addresses, &count) != 0) return out; + for (int i = 0; i < count; i++) { + uv_interface_address_t *a = &addresses[i]; + bool v6 = a->address.address4.sin_family == AF_INET6; + char address[INET6_ADDRSTRLEN] = {0}, netmask[INET6_ADDRSTRLEN] = {0}; + int prefix; + if (v6) { + uv_ip6_name(&a->address.address6, address, sizeof(address)); + uv_ip6_name(&a->netmask.netmask6, netmask, sizeof(netmask)); + prefix = sxn_mask_prefix((const uint8_t *)&a->netmask.netmask6.sin6_addr, 16); + } else { + uv_ip4_name(&a->address.address4, address, sizeof(address)); + uv_ip4_name(&a->netmask.netmask4, netmask, sizeof(netmask)); + prefix = sxn_mask_prefix((const uint8_t *)&a->netmask.netmask4.sin_addr, 4); + } + char mac[18]; + snprintf(mac, sizeof(mac), "%02x:%02x:%02x:%02x:%02x:%02x", + (unsigned char)a->phys_addr[0], (unsigned char)a->phys_addr[1], + (unsigned char)a->phys_addr[2], (unsigned char)a->phys_addr[3], + (unsigned char)a->phys_addr[4], (unsigned char)a->phys_addr[5]); + char cidr[INET6_ADDRSTRLEN + 8]; + snprintf(cidr, sizeof(cidr), "%s/%d", address, prefix); + + JSValue entry = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, entry, "address", JS_NewString(ctx, address)); + JS_SetPropertyStr(ctx, entry, "netmask", JS_NewString(ctx, netmask)); + JS_SetPropertyStr(ctx, entry, "family", JS_NewString(ctx, v6 ? "IPv6" : "IPv4")); + JS_SetPropertyStr(ctx, entry, "mac", JS_NewString(ctx, mac)); + JS_SetPropertyStr(ctx, entry, "internal", JS_NewBool(ctx, a->is_internal)); + JS_SetPropertyStr(ctx, entry, "cidr", JS_NewString(ctx, cidr)); + if (v6) + JS_SetPropertyStr(ctx, entry, "scopeid", JS_NewInt32(ctx, (int32_t)a->address.address6.sin6_scope_id)); + + JSValue list = JS_GetPropertyStr(ctx, out, a->name); + if (!JS_IsArray(list)) { + JS_FreeValue(ctx, list); + list = JS_NewArray(ctx); + JS_SetPropertyStr(ctx, out, a->name, JS_DupValue(ctx, list)); + } + uint32_t length = 0; + JSValue size = JS_GetPropertyStr(ctx, list, "length"); + JS_ToUint32(ctx, &length, size); + JS_FreeValue(ctx, size); + JS_SetPropertyUint32(ctx, list, length, entry); + JS_FreeValue(ctx, list); + } + uv_free_interface_addresses(addresses, count); + return out; +} + +static JSValue sxn_write_stderr(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_UNDEFINED; + size_t length = 0; + const char *text = JS_ToCStringLen(ctx, &length, argv[0]); + if (!text) return JS_EXCEPTION; + fwrite(text, 1, length, stderr); + fflush(stderr); + JS_FreeCString(ctx, text); + return JS_UNDEFINED; +} + static JSValue sxn_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { if (argc < 2) return JS_ThrowTypeError(ctx, "Sxn.write(destination, data) requires two arguments"); const char *path = JS_ToCString(ctx, argv[0]); size_t length = 0; @@ -1866,11 +2981,12 @@ int sxn_install_network(JSContext *ctx) { JSValue global = JS_GetGlobalObject(ctx); JSValue runtime = JS_NewObject(ctx); - JS_SetPropertyStr(ctx, runtime, "version", JS_NewString(ctx, "0.0.1")); + JS_SetPropertyStr(ctx, runtime, "version", JS_NewString(ctx, "0.0.2")); JS_SetPropertyStr(ctx, runtime, "serve", JS_NewCFunction(ctx, js_serve, "serve", 2)); JS_SetPropertyStr(ctx, runtime, "file", JS_NewCFunction(ctx, sxn_file, "file", 1)); JS_SetPropertyStr(ctx, runtime, "write", JS_NewCFunction(ctx, sxn_write, "write", 2)); JS_SetPropertyStr(ctx, runtime, "memoryUsage", JS_NewCFunction(ctx, sxn_memory_usage, "memoryUsage", 0)); + JS_SetPropertyStr(ctx, runtime, "gc", JS_NewCFunction(ctx, sxn_gc, "gc", 0)); sxn_ffi_init(ctx); JS_SetPropertyStr(ctx, runtime, "ffi", JS_NewCFunction(ctx, sxn_ffi, "ffi", 4)); JS_SetPropertyStr(ctx, global, "Sxn", runtime); @@ -1880,9 +2996,27 @@ int sxn_install_network(JSContext *ctx) { networking primitive, the monotonic clock, and OpenSSL-backed crypto); bootstrap.js builds the spec-shaped globals on top of them and installs fetch/TextEncoder/URL/Headers/etc. on `global` itself. */ - JS_SetPropertyStr(ctx, global, "__sxnFetchRaw", JS_NewCFunction(ctx, js_sxn_fetch_raw, "__sxnFetchRaw", 4)); + JS_SetPropertyStr(ctx, global, "__sxnFetchRaw", JS_NewCFunction(ctx, js_sxn_fetch_raw, "__sxnFetchRaw", 5)); /* Named "now" because bootstrap.js binds this straight onto performance rather than wrapping it, so this is the function user code sees. */ + JS_SetPropertyStr(ctx, global, "__sxnParseJSONBytes", JS_NewCFunction(ctx, sxn_parse_json_bytes, "__sxnParseJSONBytes", 3)); + JS_SetPropertyStr(ctx, global, "__sxnStat", JS_NewCFunction(ctx, sxn_stat, "__sxnStat", 3)); + JS_SetPropertyStr(ctx, global, "__sxnFsConstants", JS_NewCFunction(ctx, js_fs_constants, "__sxnFsConstants", 0)); + JS_SetPropertyStr(ctx, global, "__sxnUdpOpen", JS_NewCFunction(ctx, js_udp_open, "__sxnUdpOpen", 2)); + JS_SetPropertyStr(ctx, global, "__sxnUdpBind", JS_NewCFunction(ctx, js_udp_bind, "__sxnUdpBind", 3)); + JS_SetPropertyStr(ctx, global, "__sxnUdpSend", JS_NewCFunction(ctx, js_udp_send, "__sxnUdpSend", 4)); + JS_SetPropertyStr(ctx, global, "__sxnUdpClose", JS_NewCFunction(ctx, js_udp_close, "__sxnUdpClose", 1)); + JS_SetPropertyStr(ctx, global, "__sxnSpawnSync", JS_NewCFunction(ctx, js_spawn_sync, "__sxnSpawnSync", 3)); + JS_SetPropertyStr(ctx, global, "__sxnDnsLookup", JS_NewCFunction(ctx, js_dns_lookup, "__sxnDnsLookup", 2)); + JS_SetPropertyStr(ctx, global, "__sxnOsHostname", JS_NewCFunction(ctx, sxn_os_hostname, "__sxnOsHostname", 0)); + JS_SetPropertyStr(ctx, global, "__sxnOsHomedir", JS_NewCFunctionMagic(ctx, sxn_os_dir, "__sxnOsHomedir", 0, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, global, "__sxnOsTmpdir", JS_NewCFunctionMagic(ctx, sxn_os_dir, "__sxnOsTmpdir", 0, JS_CFUNC_generic_magic, 1)); + JS_SetPropertyStr(ctx, global, "__sxnOsUname", JS_NewCFunction(ctx, sxn_os_uname, "__sxnOsUname", 0)); + JS_SetPropertyStr(ctx, global, "__sxnOsNumbers", JS_NewCFunction(ctx, sxn_os_numbers, "__sxnOsNumbers", 0)); + JS_SetPropertyStr(ctx, global, "__sxnOsCpus", JS_NewCFunction(ctx, sxn_os_cpus, "__sxnOsCpus", 0)); + JS_SetPropertyStr(ctx, global, "__sxnOsInterfaces", JS_NewCFunction(ctx, sxn_os_interfaces, "__sxnOsInterfaces", 0)); + JS_SetPropertyStr(ctx, global, "__sxnPid", JS_NewInt32(ctx, (int32_t)uv_os_getpid())); + JS_SetPropertyStr(ctx, global, "__sxnWriteStderr", JS_NewCFunction(ctx, sxn_write_stderr, "__sxnWriteStderr", 1)); JS_SetPropertyStr(ctx, global, "__sxnNow", JS_NewCFunction(ctx, sxn_now, "now", 0)); #ifdef SXN_ABLATE_FUSION JS_SetPropertyStr(ctx, global, "__ablNop", @@ -1895,6 +3029,9 @@ int sxn_install_network(JSContext *ctx) { JS_NewCFunction(ctx, sxn_abl_emit, "__ablEmit", 2)); #endif JS_SetPropertyStr(ctx, global, "__sxnRandomBytes", JS_NewCFunction(ctx, sxn_random_bytes, "__sxnRandomBytes", 1)); + JS_SetPropertyStr(ctx, global, "__sxnBytesCompare", JS_NewCFunction(ctx, sxn_bytes_compare, "__sxnBytesCompare", 2)); + JS_SetPropertyStr(ctx, global, "__sxnHmac", JS_NewCFunction(ctx, sxn_hmac, "__sxnHmac", 3)); + JS_SetPropertyStr(ctx, global, "__sxnTimingSafeEqual", JS_NewCFunction(ctx, sxn_timing_safe_equal, "__sxnTimingSafeEqual", 2)); JS_SetPropertyStr(ctx, global, "__sxnDigest", JS_NewCFunction(ctx, sxn_digest, "__sxnDigest", 2)); /* Consumed only by bootstrap.js's TextEncoder/TextDecoder. */ JS_SetPropertyStr(ctx, global, "__sxnUtf8Encode", JS_NewCFunction(ctx, sxn_utf8_encode, "__sxnUtf8Encode", 1)); @@ -1936,9 +3073,16 @@ int sxn_install_network(JSContext *ctx) { forever -- the promise could only be settled by work that never got a chance to run. Mirrors js_std_await's contract: consumes obj, returns the fulfilled value or throws the rejection, and passes non-promises through. */ +/* Defined below with the rest of the idle-sweep machinery. Both loops in this + file call it: a module with top-level await never reaches + sxn_run_event_loop, so hooking only that one leaves every `await` daemon + uncollected. */ +static void sxn_maybe_idle_gc(JSContext *ctx, JSRuntime *rt, uint64_t blocked_ns); + JSValue sxn_await_with_loop(JSContext *ctx, JSValue obj) { JSRuntime *rt = JS_GetRuntime(ctx); uv_loop_t *loop = sxn_loop(); + uint64_t blocked_ns = 0, before; for (;;) { int state = JS_PromiseState(ctx, obj); if (state == JS_PROMISE_FULFILLED) { @@ -1965,25 +3109,112 @@ JSValue sxn_await_with_loop(JSContext *ctx, JSValue obj) { blocks until something is ready rather than spinning. If nothing is pending either, the promise can never settle -- return it and let the caller see a still-pending module rather than hang. */ - if (!uv_run(loop, UV_RUN_ONCE) && !JS_IsJobPending(rt)) + sxn_maybe_idle_gc(ctx, rt, blocked_ns); + before = uv_hrtime(); + int more_handles = uv_run(loop, UV_RUN_ONCE); + blocked_ns = uv_hrtime() - before; + if (!more_handles && !JS_IsJobPending(rt)) return obj; } } +/* Set by the rejection tracker in src/main.c when a promise is rejected with + nothing watching it. The loop below runs on every batch of I/O, so it asks + this variable rather than the global object: with no rejection in flight + the whole report costs one comparison. */ +int sxn_rejections_pending = 0; + +/* Calls the JavaScript half of the rejection report, if it is installed. */ +static void sxn_flush_rejections(JSContext *ctx) { + if (!sxn_rejections_pending) return; + sxn_rejections_pending = 0; + JSValue global = JS_GetGlobalObject(ctx); + JSValue fn = JS_GetPropertyStr(ctx, global, "__sxnFlushRejections"); + if (JS_IsFunction(ctx, fn)) { + JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, 0, NULL); + if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx)); + JS_FreeValue(ctx, r); + } + JS_FreeValue(ctx, fn); + JS_FreeValue(ctx, global); +} + +/* --- Idle cycle sweeping ------------------------------------------------- + QuickJS only collects from an allocation, so a process that stops + allocating stops collecting. That is fine for a script and wrong for a + daemon: a burst that leaves cyclic garbage behind and then goes quiet keeps + every byte of it, and because js_trigger_gc sized the threshold for the + peak, the next burst has to be half again as large before anything + notices. Measured, 200k cyclic objects dropped and then 96 MB of further + churn produced zero collections and held 229 MB. + + The loop below knows something no allocation site does: when it is about to + block in uv_run there is no request in flight, so a collection costs + nothing anyone is waiting on. The gate is deliberately narrow. + + - Above a floor, 32 MB by default. Under it there is not enough to reclaim + to be worth a pause, so a small server keeps exactly the pause profile it + has today and this code is a load and a compare. + - The previous uv_run actually blocked for a while. That is the direct + measurement of "quiet" and it is why this is timed rather than inferred + from the allocation rate: a first attempt gated on tracked size not + growing between turns never fired, because a loop with any timer on it + allocates a trickle every turn and the size always crept up. + - At least half a second since the last sweep, so a loop woken constantly + by short timers cannot turn this into a collection per turn. */ +#define SXN_IDLE_GC_FLOOR_DEFAULT (32u * 1024u * 1024u) +#define SXN_IDLE_GC_INTERVAL_NS (500u * 1000u * 1000u) +#define SXN_IDLE_GC_BLOCKED_NS (20u * 1000u * 1000u) + +static int sxn_idle_gc_enabled = 1; +static size_t sxn_idle_gc_floor = SXN_IDLE_GC_FLOOR_DEFAULT; + +void sxn_configure_idle_gc(int enabled, size_t floor_bytes) { + sxn_idle_gc_enabled = enabled; + if (floor_bytes) sxn_idle_gc_floor = floor_bytes; +} + +/* blocked_ns is how long the previous uv_run waited for work. */ +static void sxn_maybe_idle_gc(JSContext *ctx, JSRuntime *rt, uint64_t blocked_ns) { + static uint64_t last_sweep_ns; + uint64_t now; + + if (!sxn_idle_gc_enabled) return; + if (blocked_ns < SXN_IDLE_GC_BLOCKED_NS) return; + if (JS_GetMallocSize(rt) < sxn_idle_gc_floor) return; + now = uv_hrtime(); + if (last_sweep_ns && now - last_sweep_ns < SXN_IDLE_GC_INTERVAL_NS) return; + last_sweep_ns = now; + sxn_free_ee_memo(ctx); + sxn_sweep_cycles(rt); +} + int sxn_run_event_loop(JSContext *ctx) { uv_loop_t *loop = sxn_loop(); JSRuntime *rt = JS_GetRuntime(ctx); + uint64_t blocked_ns = 0; for (;;) { JSContext *ctx1; int err; + uint64_t before; while ((err = JS_ExecutePendingJob(rt, &ctx1)) > 0) {} if (err < 0) break; + /* Every job that could still settle a rejected promise has now run, + so anything still on the list is unhandled: report it (which is + what fires onunhandledrejection) before waiting for more I/O. */ + sxn_flush_rejections(ctx); + /* Nothing is in flight here and uv_run is about to block, so this is + the cheapest moment in the process to spend a collection -- and how + long the last one blocked says whether there was anything to do. */ + sxn_maybe_idle_gc(ctx, rt, blocked_ns); /* UV_RUN_ONCE blocks (no busy-loop) until the next batch of I/O is ready, then returns so we can drain any jobs it just enqueued before waiting on the next batch. Exits once nothing is left: no active/ref'd handles (e.g. no serve() was ever called) and no pending job, which is what keeps a plain script's exit identical to before this loop existed. */ + before = uv_hrtime(); int more_handles = uv_run(loop, UV_RUN_ONCE); + blocked_ns = uv_hrtime() - before; if (!more_handles && !JS_IsJobPending(rt)) break; } return JS_HasException(ctx); diff --git a/src/node.c b/src/node.c index d155740..925a57c 100644 --- a/src/node.c +++ b/src/node.c @@ -38,11 +38,36 @@ extern char **environ; that genuinely need C: real cwd/env access, exit, and safe signal delivery. Same split as bootstrap.js / network.c (Task 2). */ +/* process.cwd() is asked constantly -- every relative path a package + resolves goes through it -- and getcwd() is a system call that walks the + directory back to the root: 7 microseconds here. Node caches it, and so + does this, with process.chdir() below as the only thing that can change + it. */ +static char sxn_cwd_cache[4096]; + static JSValue js_sxn_cwd(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; - char buf[4096]; - if (!getcwd(buf, sizeof(buf))) return JS_ThrowInternalError(ctx, "getcwd failed: %s", strerror(errno)); - return JS_NewString(ctx, buf); + if (!sxn_cwd_cache[0] && !getcwd(sxn_cwd_cache, sizeof(sxn_cwd_cache))) + return JS_ThrowInternalError(ctx, "getcwd failed: %s", strerror(errno)); + return JS_NewString(ctx, sxn_cwd_cache); +} + +static JSValue js_sxn_chdir(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *dir = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!dir) return JS_ThrowTypeError(ctx, "chdir(directory) requires a directory"); + int rc = chdir(dir); + if (rc != 0) { + JSValue error = JS_ThrowInternalError(ctx, "chdir %s: %s", dir, strerror(errno)); + JSValue exception = JS_GetException(ctx); + JS_SetPropertyStr(ctx, exception, "code", JS_NewString(ctx, "ENOENT")); + JS_Throw(ctx, exception); + JS_FreeCString(ctx, dir); + return error; + } + JS_FreeCString(ctx, dir); + sxn_cwd_cache[0] = '\0'; /* the cache is what chdir invalidates */ + return JS_UNDEFINED; } /* --- process.env: native exotic object, phase 1 of replacing node_compat.js @@ -167,18 +192,31 @@ static JSValue js_sxn_write_file_sync(JSContext *ctx, JSValueConst this_val, int (void)this_val; const char *path = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; if (!path) return JS_ThrowTypeError(ctx, "expected a path"); + /* Bytes are written as they are. Everything went through + JS_ToCStringLen before, which turns a Buffer into its decimal digits + and any byte that is not valid UTF-8 into the replacement character. */ size_t length = 0; - const char *data = argc > 1 ? JS_ToCStringLen(ctx, &length, argv[1]) : NULL; + const char *data = NULL; + const char *owned = NULL; + if (argc > 1) { + uint8_t *bytes = JS_GetUint8Array(ctx, &length, argv[1]); + if (bytes) data = (const char *)bytes; + else { + JS_FreeValue(ctx, JS_GetException(ctx)); + owned = JS_ToCStringLen(ctx, &length, argv[1]); + data = owned; + } + } if (!data) { JS_FreeCString(ctx, path); return JS_ThrowTypeError(ctx, "expected data"); } FILE *file = fopen(path, "wb"); if (!file) { JSValue err = JS_ThrowInternalError(ctx, "cannot write '%s': %s", path, strerror(errno)); - JS_FreeCString(ctx, path); JS_FreeCString(ctx, data); + JS_FreeCString(ctx, path); JS_FreeCString(ctx, owned); return err; } size_t written = fwrite(data, 1, length, file); bool failed = fclose(file) != 0 || written != length; - JS_FreeCString(ctx, path); JS_FreeCString(ctx, data); + JS_FreeCString(ctx, path); JS_FreeCString(ctx, owned); if (failed) return JS_ThrowInternalError(ctx, "file write failed"); return JS_UNDEFINED; } @@ -322,6 +360,107 @@ static uint32_t sxn_ee_length(JSContext *ctx, JSValueConst list) { return len; } +/* EventEmitter.defaultMaxListeners, kept here rather than as a plain property + on the class because the check below is native. node_compat.js exposes it as + an accessor pair so `require('events').defaultMaxListeners = 20` still + works. Zero or less means unlimited, as in Node. */ +static int32_t sxn_ee_default_max_listeners = 10; + +static JSValue js_ee_default_max(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + (void)this_val; + if (argc > 0) { + int32_t v = 0; + if (JS_ToInt32(ctx, &v, argv[0])) return JS_EXCEPTION; + sxn_ee_default_max_listeners = v; + } + return JS_NewInt32(ctx, sxn_ee_default_max_listeners); +} + +/* A listener count crossing the limit is the standard signal that handlers are + being added and never removed -- the leak that grows a long-running server + one closure at a time. Node warns once per emitter and event; so does this, + marking the emitter rather than counting again, so a server registering + thousands of listeners prints one line and not thousands. + + The marker lives on the emitter as `_warnedEvents`, a null-prototype object + keyed by event name, which is deliberately not `_events`: that object's + exact shape is what emit's fast path and tests/fixtures/events_singleton.mjs + both read. */ +static void sxn_ee_check_max_listeners(JSContext *ctx, JSValueConst this_val, + JSAtom type, uint32_t count) { + JSValue max_val, warned, seen, warning, emit; + int32_t max = sxn_ee_default_max_listeners; + + max_val = JS_GetPropertyStr(ctx, this_val, "_maxListeners"); + if (JS_IsNumber(max_val)) JS_ToInt32(ctx, &max, max_val); + JS_FreeValue(ctx, max_val); + if (max <= 0 || count <= (uint32_t)max) return; + + warned = JS_GetPropertyStr(ctx, this_val, "_warnedEvents"); + if (!JS_IsObject(warned)) { + JS_FreeValue(ctx, warned); + warned = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(warned)) { JS_FreeValue(ctx, warned); return; } + if (JS_SetPropertyStr(ctx, this_val, "_warnedEvents", + JS_DupValue(ctx, warned)) < 0) { + JS_FreeValue(ctx, warned); + return; + } + } + seen = JS_GetProperty(ctx, warned, type); + if (JS_ToBool(ctx, seen)) { JS_FreeValue(ctx, seen); JS_FreeValue(ctx, warned); return; } + JS_FreeValue(ctx, seen); + JS_SetProperty(ctx, warned, type, JS_TRUE); + JS_FreeValue(ctx, warned); + + /* Node's own shape and wording: an Error named MaxListenersExceededWarning + whose message names the count, the emitter and the limit. Matching the + object rather than just the text is what lets a `process.on('warning')` + handler written against Node work unchanged. */ + { + const char *name = JS_AtomToCString(ctx, type); + const char *owner = NULL; + JSValue ctor = JS_GetPropertyStr(ctx, this_val, "constructor"); + JSValue ctor_name = JS_IsObject(ctor) + ? JS_GetPropertyStr(ctx, ctor, "name") : JS_UNDEFINED; + char message[320]; + if (JS_IsString(ctor_name)) owner = JS_ToCString(ctx, ctor_name); + snprintf(message, sizeof(message), + "Possible EventEmitter memory leak detected. " + "%u %s listeners added to [%s]. MaxListeners is %d. " + "Use emitter.setMaxListeners() to increase limit", + count, name ? name : "?", owner ? owner : "EventEmitter", max); + if (owner) JS_FreeCString(ctx, owner); + JS_FreeValue(ctx, ctor_name); + JS_FreeValue(ctx, ctor); + JS_FreeCString(ctx, name); + warning = JS_NewError(ctx); + if (JS_IsException(warning)) return; + JS_SetPropertyStr(ctx, warning, "message", JS_NewString(ctx, message)); + JS_SetPropertyStr(ctx, warning, "name", + JS_NewString(ctx, "MaxListenersExceededWarning")); + } + /* process.emitWarning, if node_compat.js has been installed. A bare + runtime with no `process` simply does not warn. */ + emit = JS_UNDEFINED; + { + JSValue global = JS_GetGlobalObject(ctx); + JSValue process = JS_GetPropertyStr(ctx, global, "process"); + if (JS_IsObject(process)) emit = JS_GetPropertyStr(ctx, process, "emitWarning"); + JS_FreeValue(ctx, process); + JS_FreeValue(ctx, global); + } + if (JS_IsFunction(ctx, emit)) { + JSValueConst args[1] = { warning }; + JSValue r = JS_Call(ctx, emit, JS_UNDEFINED, 1, args); + if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx)); + JS_FreeValue(ctx, r); + } + JS_FreeValue(ctx, emit); + JS_FreeValue(ctx, warning); +} + static JSValue js_ee_on(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { sxn_ee_gen++; /* listener set changes: invalidate the emit memo */ if (argc < 2 || !JS_IsFunction(ctx, argv[1])) return JS_ThrowTypeError(ctx, "listener must be a function"); @@ -329,20 +468,26 @@ static JSValue js_ee_on(JSContext *ctx, JSValueConst this_val, int argc, JSValue if (type == JS_ATOM_NULL) return JS_EXCEPTION; JSValue events = sxn_ee_events(ctx, this_val); JSValue list = JS_GetProperty(ctx, events, type); + uint32_t count; if (JS_IsUndefined(list)) { /* Node stores the common one-listener case directly as a function; promote to a fast array only when another listener arrives. */ JS_SetProperty(ctx, events, type, JS_DupValue(ctx, argv[1])); + count = 1; } else if (JS_IsFunction(ctx, list)) { JSValue promoted = JS_NewArray(ctx); JS_SetPropertyUint32(ctx, promoted, 0, list); /* consumes list */ JS_SetPropertyUint32(ctx, promoted, 1, JS_DupValue(ctx, argv[1])); JS_SetProperty(ctx, events, type, promoted); list = JS_UNDEFINED; /* promoted now owns the old function */ + count = 2; } else { - JS_SetPropertyUint32(ctx, list, sxn_ee_length(ctx, list), JS_DupValue(ctx, argv[1])); + count = sxn_ee_length(ctx, list); + JS_SetPropertyUint32(ctx, list, count, JS_DupValue(ctx, argv[1])); + count += 1; } JS_FreeValue(ctx, list); + sxn_ee_check_max_listeners(ctx, this_val, type, count); /* Arm the call-site emit fusion for the sole-listener case, the only shape it handles. Registration is where the layout is known; the call site re-validates it by shape and slot on every call, and reads sxn_ee_gen, @@ -363,6 +508,11 @@ static JSValue js_ee_on(JSContext *ctx, JSValueConst this_val, int argc, JSValue return JS_DupValue(ctx, this_val); } +/* How many emits are on the stack. Process-wide for the same reason the + zlib cache above is, and it only ever chooses between two correct paths + in off(), so a stale value would cost speed rather than correctness. */ +static int sxn_ee_emit_depth; + static JSValue js_ee_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { sxn_ee_gen++; /* listener set changes: invalidate the emit memo */ if (argc < 1) return JS_ThrowTypeError(ctx, "expected an event type"); @@ -389,16 +539,60 @@ static JSValue js_ee_off(JSContext *ctx, JSValueConst this_val, int argc, JSValu return JS_DupValue(ctx, this_val); } JSValueConst listener = argc > 1 ? argv[1] : JS_UNDEFINED; - JSValue out = JS_NewArray(ctx); - uint32_t out_len = 0; + /* Node removes the first match only, so the list is scanned until one is + found and then closed up in place. This used to build a second array + and copy every listener into it, which allocated on every removal and + walked the whole list even when the match was the first entry -- + expensive on an emitter many streams have piped into. */ + if (sxn_ee_emit_depth > 0) { + /* An emit is walking this list: hand it a copy to keep walking. */ + JSValue kept = JS_NewArray(ctx); + uint32_t kept_len = 0; + bool dropped = false; + for (uint32_t i = 0; i < len; i++) { + JSValue l = JS_GetPropertyUint32(ctx, list, i); + bool matches = !dropped && JS_IsStrictEqual(ctx, l, listener); + if (!matches && !dropped) { + JSValue original = JS_GetPropertyStr(ctx, l, "_original"); + matches = !JS_IsUndefined(original) && JS_IsStrictEqual(ctx, original, listener); + JS_FreeValue(ctx, original); + } + if (matches) { dropped = true; JS_FreeValue(ctx, l); } + else JS_SetPropertyUint32(ctx, kept, kept_len++, l); + } + if (kept_len == 0) JS_DeleteProperty(ctx, events, type, 0); + else if (kept_len == 1) { + JSValue single = JS_GetPropertyUint32(ctx, kept, 0); + JS_SetProperty(ctx, events, type, single); + } else { + JS_SetProperty(ctx, events, type, JS_DupValue(ctx, kept)); + } + JS_FreeValue(ctx, kept); + JS_FreeValue(ctx, list); JS_FreeValue(ctx, events); JS_FreeAtom(ctx, type); + return JS_DupValue(ctx, this_val); + } + uint32_t found = len; for (uint32_t i = 0; i < len; i++) { JSValue l = JS_GetPropertyUint32(ctx, list, i); - JSValue original = JS_GetPropertyStr(ctx, l, "_original"); - bool matches = JS_IsStrictEqual(ctx, l, listener) || (!JS_IsUndefined(original) && JS_IsStrictEqual(ctx, original, listener)); - JS_FreeValue(ctx, original); - if (matches) JS_FreeValue(ctx, l); - else JS_SetPropertyUint32(ctx, out, out_len++, l); + bool matches = JS_IsStrictEqual(ctx, l, listener); + if (!matches) { + /* once() stores a wrapper, and off(original) has to find it. */ + JSValue original = JS_GetPropertyStr(ctx, l, "_original"); + matches = !JS_IsUndefined(original) && JS_IsStrictEqual(ctx, original, listener); + JS_FreeValue(ctx, original); + } + JS_FreeValue(ctx, l); + if (matches) { found = i; break; } + } + if (found == len) { + JS_FreeValue(ctx, list); JS_FreeValue(ctx, events); JS_FreeAtom(ctx, type); + return JS_DupValue(ctx, this_val); } + for (uint32_t i = found + 1; i < len; i++) + JS_SetPropertyUint32(ctx, list, i - 1, JS_GetPropertyUint32(ctx, list, i)); + uint32_t out_len = len - 1; + JS_SetPropertyStr(ctx, list, "length", JS_NewUint32(ctx, out_len)); + JSValue out = JS_DupValue(ctx, list); if (out_len == 0) { JS_DeleteProperty(ctx, events, type, 0); JS_FreeValue(ctx, out); @@ -525,6 +719,11 @@ static JSValue js_ee_emit(JSContext *ctx, JSValueConst this_val, int argc, JSVal return JS_Throw(ctx, err); } if (type_owned) JS_FreeAtom(ctx, type); + /* A listener removed while this loop is running must not disturb it -- + Node emits to the set of listeners that existed when emit started. off() + reads this depth and copies the list instead of closing it up in place + when it is not zero. */ + sxn_ee_emit_depth++; for (uint32_t i = 0; i < n; i++) { JSValue l; if (fast) { @@ -536,9 +735,14 @@ static JSValue js_ee_emit(JSContext *ctx, JSValueConst this_val, int argc, JSVal } JSValue ret = JS_Call(ctx, l, this_val, argc - 1, argv + 1); JS_FreeValue(ctx, l); - if (JS_IsException(ret)) { if (list_owned) JS_FreeValue(ctx, list); return ret; } + if (JS_IsException(ret)) { + sxn_ee_emit_depth--; + if (list_owned) JS_FreeValue(ctx, list); + return ret; + } JS_FreeValue(ctx, ret); } + sxn_ee_emit_depth--; if (list_owned) JS_FreeValue(ctx, list); return JS_NewBool(ctx, true); } @@ -845,17 +1049,41 @@ static JSValue js_path_posix_basename(JSContext *ctx, JSValueConst this_val, int return result; } +/* Node's own rule, which a simpler "last dot wins" gets wrong for a name + that is all dots: extname("..") is "", not ".". */ +static char *sxn_posix_extname_core(const char *p) { + size_t len = strlen(p); + long start_dot = -1, start_part = 0, end = -1; + bool matched_slash = true; + int pre_dot = 0; + for (long i = (long)len - 1; i >= 0; i--) { + char c = p[i]; + if (c == '/') { + if (!matched_slash) { start_part = i + 1; break; } + continue; + } + if (end == -1) { matched_slash = false; end = i + 1; } + if (c == '.') { + if (start_dot == -1) start_dot = i; + else if (pre_dot != 1) pre_dot = 1; + } else if (start_dot != -1) { + pre_dot = -1; + } + } + if (start_dot == -1 || end == -1 || pre_dot == 0 + || (pre_dot == 1 && start_dot == end - 1 && start_dot == start_part + 1)) + return sxn_strndup("", 0); + return sxn_strndup(p + start_dot, (size_t)(end - start_dot)); +} + static JSValue js_path_posix_extname(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; const char *p = argc > 0 ? JS_ToCString(ctx, argv[0]) : JS_ToCString(ctx, JS_UNDEFINED); if (!p) return JS_EXCEPTION; - char *base = sxn_posix_basename_core(p, NULL); + char *ext = sxn_posix_extname_core(p); JS_FreeCString(ctx, p); - long dot = -1; - size_t blen = strlen(base); - for (long i = (long)blen - 1; i >= 0; i--) if (base[i] == '.') { dot = i; break; } - JSValue result = (dot <= 0) ? JS_NewString(ctx, "") : JS_NewStringLen(ctx, base + dot, blen - (size_t)dot); - free(base); + JSValue result = JS_NewString(ctx, ext); + free(ext); return result; } @@ -905,6 +1133,13 @@ static JSValue js_path_posix_relative(JSContext *ctx, JSValueConst this_val, int which is exactly how the three pairs of Node functions differ. Streaming Gzip/Gunzip objects are built on these in node_compat.js, one call per chunk boundary being unnecessary because the whole payload is in memory. */ +/* A stream that failed mid-run is not reusable: end it and let the next + call build a fresh one. */ +static void sxn_zlib_drop(bool *live, z_stream *zs, bool compress) { + if (compress) deflateEnd(zs); else inflateEnd(zs); + *live = false; +} + static JSValue sxn_zlib_run(JSContext *ctx, JSValueConst input, int window_bits, int level, bool compress) { size_t in_len = 0; @@ -916,48 +1151,170 @@ static JSValue sxn_zlib_run(JSContext *ctx, JSValueConst input, return JS_ThrowTypeError(ctx, "zlib expects bytes"); } - z_stream zs; - memset(&zs, 0, sizeof(zs)); - int rc = compress - ? deflateInit2(&zs, level, Z_DEFLATED, window_bits, 8, Z_DEFAULT_STRATEGY) - : inflateInit2(&zs, window_bits); + /* deflateInit2 allocates the window and the hash tables -- a quarter of + a megabyte at these settings -- and deflateEnd gives them straight + back, so a run of small compressions spent most of its time in malloc. + zlib's own answer is deflateReset, which keeps the state and the + settings, so one stream per (direction, window, level) is kept here + and reset instead. See spec/NODE.md for the measurement. */ + static struct { + z_stream zs; + int window_bits, level; + bool live; + } cache[2]; + int slot = compress ? 1 : 0; + z_stream *cached = &cache[slot].zs; + int rc; + if (cache[slot].live && cache[slot].window_bits == window_bits && + (!compress || cache[slot].level == level)) { + rc = compress ? deflateReset(cached) : inflateReset(cached); + } else { + if (cache[slot].live) { + compress ? deflateEnd(cached) : inflateEnd(cached); + cache[slot].live = false; + } + memset(cached, 0, sizeof(*cached)); + rc = compress + ? deflateInit2(cached, level, Z_DEFLATED, window_bits, 8, Z_DEFAULT_STRATEGY) + : inflateInit2(cached, window_bits); + if (rc == Z_OK) { + cache[slot].live = true; + cache[slot].window_bits = window_bits; + cache[slot].level = level; + } + } if (rc != Z_OK) return JS_ThrowInternalError(ctx, "zlib init failed: %d", rc); size_t cap = in_len < 1024 ? 1024 : in_len * 2; uint8_t *out = js_malloc(ctx, cap); - if (!out) { compress ? deflateEnd(&zs) : inflateEnd(&zs); return JS_EXCEPTION; } + if (!out) { sxn_zlib_drop(&cache[slot].live, cached, compress); return JS_EXCEPTION; } - zs.next_in = in; - zs.avail_in = (uInt)in_len; + cached->next_in = in; + cached->avail_in = (uInt)in_len; size_t produced = 0; for (;;) { if (produced == cap) { size_t ncap = cap * 2; uint8_t *grown = js_realloc(ctx, out, ncap); - if (!grown) { js_free(ctx, out); compress ? deflateEnd(&zs) : inflateEnd(&zs); return JS_EXCEPTION; } + if (!grown) { js_free(ctx, out); sxn_zlib_drop(&cache[slot].live, cached, compress); return JS_EXCEPTION; } out = grown; cap = ncap; } - zs.next_out = out + produced; - zs.avail_out = (uInt)(cap - produced); - rc = compress ? deflate(&zs, Z_FINISH) : inflate(&zs, Z_FINISH); - produced = cap - zs.avail_out; + cached->next_out = out + produced; + cached->avail_out = (uInt)(cap - produced); + rc = compress ? deflate(cached, Z_FINISH) : inflate(cached, Z_FINISH); + produced = cap - cached->avail_out; if (rc == Z_STREAM_END) break; if (rc == Z_OK || rc == Z_BUF_ERROR) { - if (zs.avail_out == 0) continue; /* needs more room */ + if (cached->avail_out == 0) continue; /* needs more room */ if (!compress && rc == Z_BUF_ERROR) break; /* truncated input */ continue; } js_free(ctx, out); - compress ? deflateEnd(&zs) : inflateEnd(&zs); + sxn_zlib_drop(&cache[slot].live, cached, compress); return JS_ThrowInternalError(ctx, "zlib %s failed: %d", compress ? "deflate" : "inflate", rc); } - compress ? deflateEnd(&zs) : inflateEnd(&zs); + /* The stream stays; the next call resets it rather than rebuilding it. */ JSValue result = JS_NewUint8ArrayCopy(ctx, out, produced); js_free(ctx, out); return result; } +/* Streaming zlib, for CompressionStream/DecompressionStream and anything else + that has to hand bytes over a chunk at a time. The one-shot path above + keeps a reset stream for the whole call; this one keeps a z_stream per + object for as long as the object lives, which is what a stream needs. */ +static JSClassID sxn_zstream_class_id; + +typedef struct { z_stream zs; bool compress, open; } SxnZStream; + +static void sxn_zstream_finalizer(JSRuntime *rt, JSValue val) { + SxnZStream *s = JS_GetOpaque(val, sxn_zstream_class_id); + if (!s) return; + if (s->open) { if (s->compress) deflateEnd(&s->zs); else inflateEnd(&s->zs); } + js_free_rt(rt, s); +} + +static JSClassDef sxn_zstream_class_def = { + .class_name = "ZlibStream", + .finalizer = sxn_zstream_finalizer, +}; + +/* __sxnZlibStreamNew(windowBits, level, decompress) */ +static JSValue js_zlib_stream_new(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + int32_t window_bits = 15, level = Z_DEFAULT_COMPRESSION; + if (argc > 0) JS_ToInt32(ctx, &window_bits, argv[0]); + if (argc > 1) JS_ToInt32(ctx, &level, argv[1]); + bool decompress = argc > 2 && JS_ToBool(ctx, argv[2]); + + SxnZStream *s = js_mallocz(ctx, sizeof(*s)); + if (!s) return JS_EXCEPTION; + s->compress = !decompress; + int rc = decompress ? inflateInit2(&s->zs, window_bits) + : deflateInit2(&s->zs, level, Z_DEFLATED, window_bits, 8, Z_DEFAULT_STRATEGY); + if (rc != Z_OK) { js_free(ctx, s); return JS_ThrowInternalError(ctx, "zlib init failed: %d", rc); } + s->open = true; + + JS_NewClassID(JS_GetRuntime(ctx), &sxn_zstream_class_id); + JS_NewClass(JS_GetRuntime(ctx), sxn_zstream_class_id, &sxn_zstream_class_def); + JSValue obj = JS_NewObjectClass(ctx, sxn_zstream_class_id); + if (JS_IsException(obj)) { sxn_zstream_finalizer(JS_GetRuntime(ctx), obj); return obj; } + JS_SetOpaque(obj, s); + return obj; +} + +/* __sxnZlibStreamPush(stream, bytes, finish) -> Uint8Array of whatever came out */ +static JSValue js_zlib_stream_push(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + SxnZStream *s = argc > 0 ? JS_GetOpaque(argv[0], sxn_zstream_class_id) : NULL; + if (!s || !s->open) return JS_ThrowTypeError(ctx, "not an open zlib stream"); + size_t in_len = 0; + uint8_t *in = NULL; + if (argc > 1 && !JS_IsUndefined(argv[1]) && !JS_IsNull(argv[1])) { + in = JS_GetUint8Array(ctx, &in_len, argv[1]); + if (!in) return JS_ThrowTypeError(ctx, "zlib expects bytes"); + } + bool finish = argc > 2 && JS_ToBool(ctx, argv[2]); + + size_t cap = in_len + 64, len = 0; + uint8_t *out = js_malloc(ctx, cap); + if (!out) return JS_EXCEPTION; + s->zs.next_in = in; + s->zs.avail_in = (uInt)in_len; + int rc = Z_OK; + do { + if (len == cap) { + uint8_t *grown = js_realloc(ctx, out, cap * 2); + if (!grown) { js_free(ctx, out); return JS_EXCEPTION; } + out = grown; cap *= 2; + } + s->zs.next_out = out + len; + s->zs.avail_out = (uInt)(cap - len); + rc = s->compress ? deflate(&s->zs, finish ? Z_FINISH : Z_NO_FLUSH) + : inflate(&s->zs, finish ? Z_FINISH : Z_NO_FLUSH); + len = cap - s->zs.avail_out; + if (rc == Z_STREAM_END) break; + if (rc != Z_OK && rc != Z_BUF_ERROR) { + js_free(ctx, out); + return JS_ThrowInternalError(ctx, "zlib %s failed: %d", s->compress ? "deflate" : "inflate", rc); + } + if (rc == Z_BUF_ERROR && !finish) break; + } while (s->zs.avail_out == 0 || (finish && rc != Z_STREAM_END)); + + if (finish) { + if (!s->compress && rc != Z_STREAM_END) { + js_free(ctx, out); + return JS_ThrowInternalError(ctx, "unexpected end of compressed data"); + } + if (s->compress) deflateEnd(&s->zs); else inflateEnd(&s->zs); + s->open = false; + } + JSValue bytes = JS_NewUint8ArrayCopy(ctx, out, len); + js_free(ctx, out); + return bytes; +} + static JSValue js_zlib_deflate(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int32_t bits = 15, level = Z_DEFAULT_COMPRESSION; @@ -1481,187 +1838,3039 @@ static const char *node_util_names[] = { "isDeepStrictEqual", "types", "TextEncoder", "TextDecoder", }; static const char *node_os_names[] = { - "EOL", "platform", "arch", "type", "release", "hostname", "tmpdir", - "homedir", "endianness", "cpus", "totalmem", "freemem", "uptime", "devNull", -}; -static const char *node_querystring_names[] = { "parse", "stringify", "escape", "unescape" }; -static const char *node_url_names[] = { - "URL", "URLSearchParams", "fileURLToPath", "pathToFileURL", "format", "parse", -}; -static const char *node_net_names[] = { - "isIP", "isIPv4", "isIPv6", "Socket", "Server", - "createConnection", "connect", "createServer", -}; -static const char *node_crypto_names[] = { - "createHash", "createHmac", "randomBytes", "randomUUID", "randomFillSync", - "getRandomValues", "randomInt", "timingSafeEqual", "getHashes", - "constants", "webcrypto", "subtle", "Hash", "Hmac", -}; -static const char *node_zlib_names[] = { - "constants", "gzip", "gunzip", "deflate", "inflate", "deflateRaw", - "inflateRaw", "unzip", - "gzipSync", "gunzipSync", "deflateSync", "inflateSync", "deflateRawSync", - "inflateRawSync", "unzipSync", - "createGzip", "createGunzip", "createDeflate", "createInflate", - "createDeflateRaw", "createInflateRaw", "createUnzip", -}; -static const char *node_tty_names[] = { "isatty", "ReadStream", "WriteStream" }; -static const char *node_string_decoder_names[] = { "StringDecoder" }; -static const char *node_timers_names[] = { - "setTimeout", "clearTimeout", "setInterval", "clearInterval", - "setImmediate", "clearImmediate", "promises", -}; -static const char *node_perf_hooks_names[] = { "performance", "PerformanceObserver" }; -static const char *node_module_names[] = { "createRequire", "builtinModules", "isBuiltin" }; -static const char *node_http_names[] = { - "createServer", "request", "get", "Server", "IncomingMessage", - "ServerResponse", "STATUS_CODES", "METHODS", -}; -static const char *node_stream_names[] = { - "Readable", "Writable", "Duplex", "Transform", "PassThrough", - "Stream", "pipeline", "finished", "promises", -}; -static const char *node_assert_names[] = { - "ok", "equal", "notEqual", "strictEqual", "notStrictEqual", "deepEqual", - "deepStrictEqual", "notDeepStrictEqual", "fail", "throws", "doesNotThrow", - "match", "strict", "AssertionError", + "EOL", "platform", "arch", "type", "release", "version", "machine", + "hostname", "tmpdir", "homedir", "endianness", "cpus", + "availableParallelism", "networkInterfaces", "totalmem", "freemem", + "loadavg", "uptime", "userInfo", "devNull", "constants", }; -/* One shared init: pull the object off its global and re-export its keys. */ -typedef struct { const char *global; const char **names; size_t count; } NodeSimpleModule; +/* ---------------- node:querystring, in C ---------------- + Pure string work with no state of its own, which is what makes it worth + moving out of node_compat.js: every byte of a query string went through + split(), a regexp for "+", and decodeURIComponent per part. */ -static int node_simple_init(JSContext *ctx, JSModuleDef *m, - const char *global, const char **names, size_t count) { - JSValue obj = node_global_lookup(ctx, global); - JS_SetModuleExport(ctx, m, "default", JS_DupValue(ctx, obj)); - for (size_t i = 0; i < count; i++) - JS_SetModuleExport(ctx, m, names[i], JS_GetPropertyStr(ctx, obj, names[i])); - JS_FreeValue(ctx, obj); - return 0; +static int sxn_hex_value(unsigned char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; } -#define NODE_SIMPLE_MODULE(tag, globalname, namearr) \ - static int node_##tag##_init(JSContext *ctx, JSModuleDef *m) { \ - return node_simple_init(ctx, m, globalname, namearr, countof(namearr)); \ - } \ - static JSModuleDef *sxn_init_module_node_##tag(JSContext *ctx, const char *name) { \ - JSModuleDef *m = JS_NewCModule(ctx, name, node_##tag##_init); \ - if (!m) return NULL; \ - JS_AddModuleExport(ctx, m, "default"); \ - for (size_t i = 0; i < countof(namearr); i++) \ - JS_AddModuleExport(ctx, m, namearr[i]); \ - return m; \ +/* Percent-decoding with "+" for space, into a fresh buffer. A stray "%" is + kept as it stands, which is what Node's lenient fallback does rather than + throwing the way decodeURIComponent would. */ +static char *sxn_qs_decode(const char *src, size_t len, size_t *out_len) { + char *out = malloc(len + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)src[i]; + if (c == '+') { out[o++] = ' '; continue; } + if (c == '%' && i + 2 < len) { + int hi = sxn_hex_value((unsigned char)src[i + 1]); + int lo = sxn_hex_value((unsigned char)src[i + 2]); + if (hi >= 0 && lo >= 0) { out[o++] = (char)((hi << 4) | lo); i += 2; continue; } + } + out[o++] = (char)c; } - -NODE_SIMPLE_MODULE(util, "__sxnUtil", node_util_names) -NODE_SIMPLE_MODULE(os, "__sxnOs", node_os_names) -NODE_SIMPLE_MODULE(querystring, "__sxnQuerystring", node_querystring_names) -NODE_SIMPLE_MODULE(url, "__sxnUrl", node_url_names) -NODE_SIMPLE_MODULE(assert, "__sxnAssert", node_assert_names) -NODE_SIMPLE_MODULE(stream, "__sxnStream", node_stream_names) -NODE_SIMPLE_MODULE(http, "__sxnHttp", node_http_names) -NODE_SIMPLE_MODULE(net, "__sxnNet", node_net_names) -NODE_SIMPLE_MODULE(crypto, "__sxnCrypto", node_crypto_names) -NODE_SIMPLE_MODULE(zlib, "__sxnZlib", node_zlib_names) -NODE_SIMPLE_MODULE(tty, "__sxnTty", node_tty_names) -NODE_SIMPLE_MODULE(string_decoder, "__sxnStringDecoder", node_string_decoder_names) -NODE_SIMPLE_MODULE(timers, "__sxnTimers", node_timers_names) -NODE_SIMPLE_MODULE(perf_hooks, "__sxnPerfHooks", node_perf_hooks_names) -NODE_SIMPLE_MODULE(module, "__sxnModule", node_module_names) -static const char *node_timers_promises_names[] = { "setTimeout", "setImmediate" }; -NODE_SIMPLE_MODULE(timers_promises, "__sxnTimersPromises", node_timers_promises_names) -static const char *node_stream_promises_names[] = { "pipeline", "finished" }; -NODE_SIMPLE_MODULE(stream_promises, "__sxnStreamPromises", node_stream_promises_names) - -static const char *node_fs_export_names[] = { "readFileSync", "writeFileSync", "existsSync" }; - -static int node_fs_init(JSContext *ctx, JSModuleDef *m) { - JSValue fs = node_global_lookup(ctx, "__sxnFs"); - JS_SetModuleExport(ctx, m, "default", JS_DupValue(ctx, fs)); - for (size_t i = 0; i < countof(node_fs_export_names); i++) - JS_SetModuleExport(ctx, m, node_fs_export_names[i], JS_GetPropertyStr(ctx, fs, node_fs_export_names[i])); - JS_FreeValue(ctx, fs); - return 0; + out[o] = 0; + *out_len = o; + return out; } -static JSModuleDef *sxn_init_module_node_fs(JSContext *ctx, const char *name) { - JSModuleDef *m = JS_NewCModule(ctx, name, node_fs_init); - if (!m) return NULL; - JS_AddModuleExport(ctx, m, "default"); - for (size_t i = 0; i < countof(node_fs_export_names); i++) - JS_AddModuleExport(ctx, m, node_fs_export_names[i]); - return m; +/* The characters querystring.escape leaves alone, which are the same ones + encodeURIComponent leaves alone. */ +static bool sxn_qs_unreserved(unsigned char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') + || c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' + || c == '\'' || c == '(' || c == ')'; } -static const char *node_fs_promises_export_names[] = { "readFile", "writeFile" }; +/* A tiny growable string, so encoding does not build JS values per piece. */ +typedef struct DynStr { char *data; size_t len, cap; } DynStr; -static int node_fs_promises_init(JSContext *ctx, JSModuleDef *m) { - JSValue fsp = node_global_lookup(ctx, "__sxnFsPromises"); - JS_SetModuleExport(ctx, m, "default", JS_DupValue(ctx, fsp)); - for (size_t i = 0; i < countof(node_fs_promises_export_names); i++) - JS_SetModuleExport(ctx, m, node_fs_promises_export_names[i], JS_GetPropertyStr(ctx, fsp, node_fs_promises_export_names[i])); - JS_FreeValue(ctx, fsp); - return 0; +static void dynstr_need(DynStr *s, size_t extra) { + if (s->len + extra + 1 <= s->cap) return; + size_t cap = s->cap ? s->cap * 2 : 128; + while (cap < s->len + extra + 1) cap *= 2; + s->data = realloc(s->data, cap); + s->cap = cap; } -static JSModuleDef *sxn_init_module_node_fs_promises(JSContext *ctx, const char *name) { - JSModuleDef *m = JS_NewCModule(ctx, name, node_fs_promises_init); - if (!m) return NULL; - JS_AddModuleExport(ctx, m, "default"); - for (size_t i = 0; i < countof(node_fs_promises_export_names); i++) - JS_AddModuleExport(ctx, m, node_fs_promises_export_names[i]); - return m; +static void dynstr_add(DynStr *s, const char *src, size_t len) { + dynstr_need(s, len); + memcpy(s->data + s->len, src, len); + s->len += len; + s->data[s->len] = 0; } -static int node_process_init(JSContext *ctx, JSModuleDef *m) { - JSValue process = node_global_lookup(ctx, "process"); - JS_SetModuleExport(ctx, m, "default", process); - return 0; +static void sxn_qs_encode_into(DynStr *out, const char *src, size_t len) { + static const char hex[] = "0123456789ABCDEF"; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)src[i]; + if (sxn_qs_unreserved(c)) { + dynstr_add(out, (const char *)&c, 1); + } else { + char esc[3] = { '%', hex[c >> 4], hex[c & 15] }; + dynstr_add(out, esc, 3); + } + } } -static JSModuleDef *sxn_init_module_node_process(JSContext *ctx, const char *name) { - JSModuleDef *m = JS_NewCModule(ctx, name, node_process_init); - if (!m) return NULL; - JS_AddModuleExport(ctx, m, "default"); - return m; +static JSValue js_qs_parse(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + JSValue out = JS_NewObjectProto(ctx, JS_NULL); /* Node hands back a bare object */ + if (argc < 1 || !JS_IsString(argv[0])) return out; + size_t len = 0; + const char *str = JS_ToCStringLen(ctx, &len, argv[0]); + if (!str) { JS_FreeValue(ctx, out); return JS_EXCEPTION; } + + const char *sep = "&"; size_t sep_len = 1; + const char *eq = "="; size_t eq_len = 1; + const char *sep_owned = NULL, *eq_owned = NULL; + if (argc > 1 && JS_IsString(argv[1])) { sep_owned = JS_ToCStringLen(ctx, &sep_len, argv[1]); if (sep_owned && sep_len) sep = sep_owned; else sep_len = 1; } + if (argc > 2 && JS_IsString(argv[2])) { eq_owned = JS_ToCStringLen(ctx, &eq_len, argv[2]); if (eq_owned && eq_len) eq = eq_owned; else eq_len = 1; } + /* Node stops at 1000 keys unless told otherwise, so a query string + cannot be used to make an object with a million properties. */ + int64_t max_keys = 1000; + if (argc > 3 && JS_IsObject(argv[3])) { + JSValue limit = JS_GetPropertyStr(ctx, argv[3], "maxKeys"); + if (!JS_IsUndefined(limit)) JS_ToInt64(ctx, &max_keys, limit); + JS_FreeValue(ctx, limit); + } + + int64_t seen = 0; + size_t i = 0; + while (i <= len) { + const char *part = str + i; + const char *found = sep_len == 1 ? memchr(part, sep[0], len - i) : strstr(part, sep); + size_t part_len = found ? (size_t)(found - part) : len - i; + i += part_len + sep_len; + if (part_len == 0) { if (!found) break; continue; } + if (max_keys > 0 && seen >= max_keys) break; + + const char *split = eq_len == 1 ? memchr(part, eq[0], part_len) : NULL; + if (!split && eq_len > 1) { + for (size_t j = 0; j + eq_len <= part_len; j++) + if (!memcmp(part + j, eq, eq_len)) { split = part + j; break; } + } + size_t key_len = split ? (size_t)(split - part) : part_len; + size_t value_len = split ? part_len - key_len - eq_len : 0; + size_t dk = 0, dv = 0; + char *key = sxn_qs_decode(part, key_len, &dk); + char *value = split ? sxn_qs_decode(split + eq_len, value_len, &dv) : sxn_qs_decode("", 0, &dv); + if (!key || !value) { free(key); free(value); break; } + seen++; + + JSAtom atom = JS_NewAtomLen(ctx, key, dk); + JSValue existing = JS_GetProperty(ctx, out, atom); + JSValue fresh = JS_NewStringLen(ctx, value, dv); + if (JS_IsUndefined(existing)) { + JS_SetProperty(ctx, out, atom, fresh); + } else if (JS_IsArray(existing)) { + uint32_t length = 0; + JSValue size = JS_GetPropertyStr(ctx, existing, "length"); + JS_ToUint32(ctx, &length, size); + JS_FreeValue(ctx, size); + JS_SetPropertyUint32(ctx, existing, length, fresh); + JS_SetProperty(ctx, out, atom, existing); + existing = JS_UNDEFINED; + } else { + JSValue list = JS_NewArray(ctx); + JS_SetPropertyUint32(ctx, list, 0, existing); + JS_SetPropertyUint32(ctx, list, 1, fresh); + JS_SetProperty(ctx, out, atom, list); + existing = JS_UNDEFINED; + } + JS_FreeValue(ctx, existing); + JS_FreeAtom(ctx, atom); + free(key); + free(value); + if (!found) break; + } + JS_FreeCString(ctx, str); + if (sep_owned) JS_FreeCString(ctx, sep_owned); + if (eq_owned) JS_FreeCString(ctx, eq_owned); + return out; } -void sxn_free_node_compat(JSContext *ctx) { - /* Release the call-site fusion caches here rather than leaving them to - JS_FreeContext, which returns early when anything still holds a context - reference and would leave the cached shapes outstanding. */ - JS_DisableEmitFusion(ctx); - JS_DisableBufferLengthFusion(ctx); - /* The cached atoms below hold real references for the life of the - context; without this they show up as leaks under --leak-check (only - visible in builds where the runtime's leak dumps are compiled in). - JS_FreeAtom is a no-op for the predefined ones. */ - sxn_ee_memo_clear(ctx); - JS_FreeAtom(ctx, sxn_atom_events); - JS_FreeAtom(ctx, sxn_atom_length); - JS_FreeAtom(ctx, sxn_atom_error); - JS_FreeAtom(ctx, sxn_atom_utf8); JS_FreeAtom(ctx, sxn_atom_utf8_dash); - JS_FreeAtom(ctx, sxn_atom_hex); JS_FreeAtom(ctx, sxn_atom_base64); - JS_FreeAtom(ctx, sxn_atom_base64url); JS_FreeAtom(ctx, sxn_atom_toBase64); - JS_FreeAtom(ctx, sxn_atom_latin1); JS_FreeAtom(ctx, sxn_atom_binary); - JS_FreeAtom(ctx, sxn_atom_ascii); JS_FreeAtom(ctx, sxn_atom_ucs2); - JS_FreeAtom(ctx, sxn_atom_ucs2_dash); JS_FreeAtom(ctx, sxn_atom_utf16le); - JS_FreeAtom(ctx, sxn_atom_utf16le_dash); - sxn_atom_latin1 = sxn_atom_binary = sxn_atom_ascii = JS_ATOM_NULL; - sxn_atom_ucs2 = sxn_atom_ucs2_dash = JS_ATOM_NULL; - sxn_atom_utf16le = sxn_atom_utf16le_dash = JS_ATOM_NULL; - sxn_atom_utf8 = sxn_atom_utf8_dash = sxn_atom_hex = JS_ATOM_NULL; - sxn_atom_base64 = sxn_atom_base64url = JS_ATOM_NULL; - sxn_atom_toBase64 = JS_ATOM_NULL; - sxn_atom_events = JS_ATOM_NULL; - sxn_atom_length = JS_ATOM_NULL; - sxn_atom_error = JS_ATOM_NULL; +/* One value of an object being stringified: a string, a number, a boolean or + anything else, which Node writes as empty. */ +static void sxn_qs_add_value(JSContext *ctx, DynStr *out, JSValueConst value) { + int tag = JS_VALUE_GET_NORM_TAG(value); + if (tag == JS_TAG_STRING || tag == JS_TAG_INT || tag == JS_TAG_FLOAT64 || tag == JS_TAG_BOOL) { + size_t len = 0; + const char *text = JS_ToCStringLen(ctx, &len, value); + if (text) { sxn_qs_encode_into(out, text, len); JS_FreeCString(ctx, text); } + } } -/* Node-API lives in src/napi.c and is installed from here, not from the - runtime's own surface: loading npm's compiled addons is Node emulation. - Sxn.ffi is the other half of the pair and sits on the runtime side. */ -void sxn_install_napi(JSContext *ctx, uv_loop_t *loop); +static JSValue js_qs_stringify(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1 || !JS_IsObject(argv[0])) return JS_NewString(ctx, ""); + const char *sep = "&", *eq = "="; + const char *sep_owned = NULL, *eq_owned = NULL; + size_t sep_len = 1, eq_len = 1; + if (argc > 1 && JS_IsString(argv[1])) { sep_owned = JS_ToCStringLen(ctx, &sep_len, argv[1]); if (sep_owned) sep = sep_owned; } + if (argc > 2 && JS_IsString(argv[2])) { eq_owned = JS_ToCStringLen(ctx, &eq_len, argv[2]); if (eq_owned) eq = eq_owned; } + + JSPropertyEnum *props = NULL; + uint32_t count = 0; + DynStr out = {0}; + if (!JS_GetOwnPropertyNames(ctx, &props, &count, argv[0], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY)) { + for (uint32_t i = 0; i < count; i++) { + JSValue value = JS_GetProperty(ctx, argv[0], props[i].atom); + size_t key_len = 0; + JSValue key_value = JS_AtomToString(ctx, props[i].atom); + const char *key = JS_ToCStringLen(ctx, &key_len, key_value); + if (JS_IsArray(value)) { + uint32_t length = 0; + JSValue size = JS_GetPropertyStr(ctx, value, "length"); + JS_ToUint32(ctx, &length, size); + JS_FreeValue(ctx, size); + for (uint32_t j = 0; j < length; j++) { + if (out.len) dynstr_add(&out, sep, sep_len); + if (key) sxn_qs_encode_into(&out, key, key_len); + dynstr_add(&out, eq, eq_len); + JSValue one = JS_GetPropertyUint32(ctx, value, j); + sxn_qs_add_value(ctx, &out, one); + JS_FreeValue(ctx, one); + } + } else { + if (out.len) dynstr_add(&out, sep, sep_len); + if (key) sxn_qs_encode_into(&out, key, key_len); + dynstr_add(&out, eq, eq_len); + sxn_qs_add_value(ctx, &out, value); + } + if (key) JS_FreeCString(ctx, key); + JS_FreeValue(ctx, key_value); + JS_FreeValue(ctx, value); + } + JS_FreePropertyEnum(ctx, props, count); + } + if (sep_owned) JS_FreeCString(ctx, sep_owned); + if (eq_owned) JS_FreeCString(ctx, eq_owned); + JSValue result = JS_NewStringLen(ctx, out.data ? out.data : "", out.len); + free(out.data); + return result; +} + +static JSValue js_qs_escape(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_NewString(ctx, "undefined"); + size_t len = 0; + const char *text = JS_ToCStringLen(ctx, &len, argv[0]); + if (!text) return JS_EXCEPTION; + DynStr out = {0}; + sxn_qs_encode_into(&out, text, len); + JS_FreeCString(ctx, text); + JSValue result = JS_NewStringLen(ctx, out.data ? out.data : "", out.len); + free(out.data); + return result; +} + +static JSValue js_qs_unescape(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_NewString(ctx, "undefined"); + size_t len = 0; + const char *text = JS_ToCStringLen(ctx, &len, argv[0]); + if (!text) return JS_EXCEPTION; + size_t out_len = 0; + char *decoded = sxn_qs_decode(text, len, &out_len); + JS_FreeCString(ctx, text); + if (!decoded) return JS_ThrowOutOfMemory(ctx); + JSValue result = JS_NewStringLen(ctx, decoded, out_len); + free(decoded); + return result; +} + + +/* ---------------- node:path's win32 half, in C ---------------- + The last of path that was still JavaScript, and the last place in + node_compat.js that reached for a regexp to walk a string. Same algorithm + as the JS it replaces; separators are '\\' and '/', a root is a drive, a + UNC share or a bare separator, and comparison is case-insensitive. */ + +static bool sxn_win_sep(char c) { return c == '\\' || c == '/'; } + +typedef struct SxnWinRoot { + size_t length; /* how much of the path the root takes */ + char prefix[512]; /* what a normalized path starts with */ + char root_path[512];/* the root on its own, with a separator */ +} SxnWinRoot; + +static void sxn_win_root(const char *p, SxnWinRoot *root) { + size_t len = strlen(p); + root->length = 0; + root->prefix[0] = 0; + root->root_path[0] = 0; + /* \\server\share */ + if (len >= 2 && sxn_win_sep(p[0]) && sxn_win_sep(p[1])) { + size_t i = 2; + while (i < len && sxn_win_sep(p[i])) i++; + size_t server = i; + while (i < len && !sxn_win_sep(p[i])) i++; + if (i > server && i < len) { + size_t after_server = i; + while (i < len && sxn_win_sep(p[i])) i++; + size_t share = i; + while (i < len && !sxn_win_sep(p[i])) i++; + if (i > share) { + root->length = i; + snprintf(root->prefix, sizeof(root->prefix), "\\\\%.*s\\%.*s\\", + (int)(after_server - server), p + server, + (int)(i - share), p + share); + snprintf(root->root_path, sizeof(root->root_path), "%.*s", (int)i, p); + return; + } + } + } + /* C:\ or C: */ + if (len >= 2 && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z')) && p[1] == ':') { + bool with_sep = len >= 3 && sxn_win_sep(p[2]); + root->length = with_sep ? 3 : 2; + snprintf(root->prefix, sizeof(root->prefix), "%c:%s", p[0], with_sep ? "\\" : ""); + snprintf(root->root_path, sizeof(root->root_path), "%c:\\", p[0]); + return; + } + if (len >= 1 && sxn_win_sep(p[0])) { + root->length = 1; + snprintf(root->prefix, sizeof(root->prefix), "\\"); + snprintf(root->root_path, sizeof(root->root_path), "\\"); + } +} + +static bool sxn_win_is_absolute(const char *p) { + size_t len = strlen(p); + if (len >= 2 && sxn_win_sep(p[0]) && sxn_win_sep(p[1])) return true; + if (len >= 3 && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z')) + && p[1] == ':' && sxn_win_sep(p[2])) return true; + if (len >= 1 && sxn_win_sep(p[0])) return true; + return false; +} + +static void sxn_win_reduce(SxnStrVec *out, const char *rest, bool is_abs) { + const char *p = rest; + while (*p) { + const char *start = p; + while (*p && !sxn_win_sep(*p)) p++; + size_t len = (size_t)(p - start); + if (len == 0 || (len == 1 && start[0] == '.')) { + if (*p) p++; + continue; + } + if (len == 2 && start[0] == '.' && start[1] == '.') { + if (out->len && strcmp(out->items[out->len - 1], "..") != 0) free(out->items[--out->len]); + else if (!is_abs) sxn_strvec_push(out, sxn_strndup("..", 2)); + } else { + sxn_strvec_push(out, sxn_strndup(start, len)); + } + if (*p) p++; + } +} + +static char *sxn_win_normalize(const char *path) { + if (!*path) return strdup("."); + bool abs = sxn_win_is_absolute(path); + SxnWinRoot root; + sxn_win_root(path, &root); + const char *rest = path + root.length; + size_t rest_len = strlen(rest); + bool trailing_sep = rest_len > 0 && sxn_win_sep(rest[rest_len - 1]); + SxnStrVec segs = {0}; + sxn_win_reduce(&segs, rest, abs); + + size_t total = strlen(root.prefix) + 1; + for (size_t i = 0; i < segs.len; i++) total += strlen(segs.items[i]) + 1; + char *out = malloc(total + 2); + strcpy(out, root.prefix); + for (size_t i = 0; i < segs.len; i++) { + if (i) strcat(out, "\\"); + strcat(out, segs.items[i]); + } + if (!*out) { free(out); out = strdup("."); } + else if (trailing_sep && segs.len && out[strlen(out) - 1] != '\\') strcat(out, "\\"); + sxn_strvec_free(&segs); + return out; +} + +static char *sxn_win_join_core(const char *const *segs, int n) { + size_t total = 1; + for (int i = 0; i < n; i++) if (segs[i]) total += strlen(segs[i]) + 1; + char *joined = malloc(total + 1); + joined[0] = 0; + bool any = false; + for (int i = 0; i < n; i++) { + if (!segs[i] || !*segs[i]) continue; + if (any) strcat(joined, "\\"); + strcat(joined, segs[i]); + any = true; + } + if (!any) { free(joined); return strdup("."); } + char *out = sxn_win_normalize(joined); + free(joined); + return out; +} + +static char *sxn_win_resolve_core(const char *const *segs, int n) { + char *resolved = strdup(""); + bool absolute = false; + for (int i = n - 1; i >= -1 && !absolute; i--) { + char *seg = i >= 0 ? (segs[i] ? strdup(segs[i]) : strdup("")) : sxn_getcwd_alloc(); + if (!*seg) { free(seg); continue; } + size_t len = strlen(seg) + 1 + strlen(resolved) + 1; + char *next = malloc(len); + snprintf(next, len, "%s\\%s", seg, resolved); + free(resolved); + resolved = next; + absolute = sxn_win_is_absolute(seg); + free(seg); + } + char *out; + if (absolute) { + out = sxn_win_normalize(resolved); + } else { + char *cwd = sxn_getcwd_alloc(); + size_t len = strlen(cwd) + 1 + strlen(resolved) + 1; + char *combined = malloc(len); + snprintf(combined, len, "%s\\%s", cwd, resolved); + out = sxn_win_normalize(combined); + free(combined); + free(cwd); + } + free(resolved); + SxnWinRoot root; + sxn_win_root(out, &root); + size_t out_len = strlen(out); + if (out_len > root.length && sxn_win_sep(out[out_len - 1])) out[out_len - 1] = 0; + return out; +} + +/* basename, dirname and extname follow Node's own loops rather than a + root-prefix model: on Windows the interesting cases -- a UNC share, a + drive-relative path, a name that is all dots -- are exactly where a + simpler model and Node disagree. */ + +/* Where the path proper starts: past a drive letter, if there is one. */ +static size_t sxn_win_root_start(const char *p, size_t len) { + if (len >= 2 && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z')) && p[1] == ':') + return 2; + return 0; +} + +static char *sxn_win_basename_core(const char *p, const char *suffix) { + size_t len = strlen(p); + size_t start = sxn_win_root_start(p, len); + long end = -1; + bool matched_slash = true; + size_t begin = start; + for (long i = (long)len - 1; i >= (long)start; i--) { + if (sxn_win_sep(p[i])) { + if (!matched_slash) { begin = (size_t)i + 1; break; } + } else if (end == -1) { + matched_slash = false; + end = i + 1; + } + } + if (end == -1) return sxn_strndup("", 0); + size_t base_len = (size_t)end - begin; + if (suffix) { + size_t slen = strlen(suffix); + if (base_len > slen && !memcmp(p + begin + base_len - slen, suffix, slen)) base_len -= slen; + } + return sxn_strndup(p + begin, base_len); +} + +static char *sxn_win_dirname_core(const char *p) { + size_t len = strlen(p); + if (len == 0) return strdup("."); + size_t root_end = 0; + size_t offset = 0; + if (len > 1 && sxn_win_sep(p[0])) { + root_end = offset = 1; + if (sxn_win_sep(p[1])) { + /* \\server\share: the root runs to the end of the share name. */ + size_t j = 2, last = j; + while (j < len && !sxn_win_sep(p[j])) j++; + if (j < len && j != last) { + last = j; + while (j < len && sxn_win_sep(p[j])) j++; + if (j < len && j != last) { + last = j; + while (j < len && !sxn_win_sep(p[j])) j++; + if (j == len) return sxn_strndup(p, len); + if (j != last) root_end = offset = j + 1; + } + } + } + } else if (sxn_win_root_start(p, len) == 2) { + root_end = len > 2 && sxn_win_sep(p[2]) ? 3 : 2; + offset = root_end; + } + long end = -1; + bool matched_slash = true; + for (long i = (long)len - 1; i >= (long)offset; i--) { + if (sxn_win_sep(p[i])) { + if (!matched_slash) { end = i; break; } + } else { + matched_slash = false; + } + } + if (end == -1) { + if (root_end == 0) return strdup("."); + return sxn_strndup(p, root_end); + } + if (end == 0) return sxn_strndup(p, 1); + return sxn_strndup(p, (size_t)end); +} + +static char *sxn_win_extname_core(const char *p) { + size_t len = strlen(p); + size_t start = sxn_win_root_start(p, len); + long start_dot = -1, start_part = (long)start, end = -1; + bool matched_slash = true; + /* 0 = nothing seen yet, 1 = only dots so far, -1 = a real character. */ + int pre_dot = 0; + for (long i = (long)len - 1; i >= (long)start; i--) { + char c = p[i]; + if (sxn_win_sep(c)) { + if (!matched_slash) { start_part = i + 1; break; } + continue; + } + if (end == -1) { matched_slash = false; end = i + 1; } + if (c == '.') { + if (start_dot == -1) start_dot = i; + else if (pre_dot != 1) pre_dot = 1; + } else if (start_dot != -1) { + pre_dot = -1; + } + } + if (start_dot == -1 || end == -1 || pre_dot == 0 + || (pre_dot == 1 && start_dot == end - 1 && start_dot == start_part + 1)) + return sxn_strndup("", 0); + return sxn_strndup(p + start_dot, (size_t)(end - start_dot)); +} + +static JSValue js_path_win_normalize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *p = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!p) return JS_EXCEPTION; + char *out = sxn_win_normalize(p); + JS_FreeCString(ctx, p); + JSValue result = JS_NewString(ctx, out); + free(out); + return result; +} + +static JSValue js_path_win_is_absolute(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *p = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!p) return JS_EXCEPTION; + bool abs = sxn_win_is_absolute(p); + JS_FreeCString(ctx, p); + return JS_NewBool(ctx, abs); +} + +static JSValue js_path_win_join(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + return sxn_cstr_list_call(ctx, argc, argv, sxn_win_join_core); +} + +static JSValue js_path_win_resolve(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + return sxn_cstr_list_call(ctx, argc, argv, sxn_win_resolve_core); +} + +static JSValue js_path_win_dirname(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *p = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!p) return JS_EXCEPTION; + char *out = sxn_win_dirname_core(p); + JS_FreeCString(ctx, p); + JSValue result = JS_NewString(ctx, out); + free(out); + return result; +} + +static JSValue js_path_win_basename(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *p = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!p) return JS_EXCEPTION; + const char *suffix = argc > 1 && JS_IsString(argv[1]) ? JS_ToCString(ctx, argv[1]) : NULL; + char *base = sxn_win_basename_core(p, suffix); + JS_FreeCString(ctx, p); + if (suffix) JS_FreeCString(ctx, suffix); + JSValue result = JS_NewString(ctx, base); + free(base); + return result; +} + +static JSValue js_path_win_extname(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *p = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + if (!p) return JS_EXCEPTION; + char *ext = sxn_win_extname_core(p); + JS_FreeCString(ctx, p); + JSValue result = JS_NewString(ctx, ext); + free(ext); + return result; +} + +/* Case-insensitive, because Windows paths are. */ +static int sxn_win_casecmp(const char *a, const char *b) { + for (; *a && *b; a++, b++) { + int ca = (unsigned char)*a, cb = (unsigned char)*b; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) return ca - cb; + } + return (unsigned char)*a - (unsigned char)*b; +} + +static JSValue js_path_win_relative(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + const char *from_in = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; + const char *to_in = argc > 1 ? JS_ToCString(ctx, argv[1]) : NULL; + if (!from_in || !to_in) { + if (from_in) JS_FreeCString(ctx, from_in); + if (to_in) JS_FreeCString(ctx, to_in); + return JS_EXCEPTION; + } + const char *one[1]; + one[0] = from_in; char *from = sxn_win_resolve_core(one, 1); + one[0] = to_in; char *to = sxn_win_resolve_core(one, 1); + JS_FreeCString(ctx, from_in); + JS_FreeCString(ctx, to_in); + if (!strcmp(from, to)) { free(from); free(to); return JS_NewString(ctx, ""); } + + SxnStrVec fs_ = {0}, ts = {0}; + sxn_win_reduce(&fs_, from, false); + sxn_win_reduce(&ts, to, false); + size_t common = 0; + while (common < fs_.len && common < ts.len && !sxn_win_casecmp(fs_.items[common], ts.items[common])) common++; + + DynStr out = {0}; + for (size_t i = common; i < fs_.len; i++) { + if (out.len) dynstr_add(&out, "\\", 1); + dynstr_add(&out, "..", 2); + } + for (size_t i = common; i < ts.len; i++) { + if (out.len) dynstr_add(&out, "\\", 1); + dynstr_add(&out, ts.items[i], strlen(ts.items[i])); + } + sxn_strvec_free(&fs_); + sxn_strvec_free(&ts); + free(from); + free(to); + JSValue result = JS_NewStringLen(ctx, out.data ? out.data : "", out.len); + free(out.data); + return result; +} + + +/* net.isIP, from the system's own address parser. Express reaches for this + on every request that carries X-Forwarded-For, and it was two regexps and + a split per call. */ +static JSValue js_net_is_ip(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1 || !JS_IsString(argv[0])) return JS_NewInt32(ctx, 0); + const char *text = JS_ToCString(ctx, argv[0]); + if (!text) return JS_EXCEPTION; + int family = 0; + struct { uint8_t bytes[16]; } addr; + if (uv_inet_pton(AF_INET, text, &addr) == 0) { + family = 4; + } else { + /* A zone index (fe80::1%eth0) names an interface rather than part of + the address; Node accepts one and so does this, but it has to name + something -- and the address itself is never parsed with the "%" + still in it, which some platforms would accept. */ + const char *percent = strchr(text, '%'); + if (!percent) { + if (uv_inet_pton(AF_INET6, text, &addr) == 0) family = 6; + } else if (percent != text && percent[1] != 0) { + size_t len = (size_t)(percent - text); + char *bare = malloc(len + 1); + if (bare) { + memcpy(bare, text, len); + bare[len] = 0; + if (uv_inet_pton(AF_INET6, bare, &addr) == 0) family = 6; + free(bare); + } + } + } + JS_FreeCString(ctx, text); + return JS_NewInt32(ctx, family); +} + + +/* ---------------- Buffer's numeric accessors, in C ---------------- + Node has about forty of these -- readUInt32BE, writeInt16LE and the rest. + They are pure byte-to-number conversion, they are what binary protocol + code spends its time in, and none of them existed here. One function + covers them all: the magic argument carries the width, the sign, the + endianness and whether it is a float. */ + +#define SXN_NUM_WIDTH(m) ((m) & 0x0f) +#define SXN_NUM_SIGNED 0x10 +#define SXN_NUM_BIG_END 0x20 +#define SXN_NUM_FLOAT 0x40 +#define SXN_NUM_BIG_INT 0x80 + +static bool sxn_num_range(JSContext *ctx, size_t buf_len, int64_t offset, int width) { + if (offset < 0 || (uint64_t)offset + (uint64_t)width > (uint64_t)buf_len) { + JS_ThrowRangeError(ctx, "the value of \"offset\" is out of range"); + return false; + } + return true; +} + +static uint64_t sxn_read_raw(const uint8_t *p, int width, bool big_endian) { + uint64_t v = 0; + if (big_endian) for (int i = 0; i < width; i++) v = (v << 8) | p[i]; + else for (int i = width - 1; i >= 0; i--) v = (v << 8) | p[i]; + return v; +} + +static void sxn_write_raw(uint8_t *p, uint64_t v, int width, bool big_endian) { + if (big_endian) for (int i = width - 1; i >= 0; i--) { p[i] = (uint8_t)(v & 0xff); v >>= 8; } + else for (int i = 0; i < width; i++) { p[i] = (uint8_t)(v & 0xff); v >>= 8; } +} + +static JSValue js_buffer_read(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, this_val); + if (!bytes) return JS_ThrowTypeError(ctx, "not a Buffer"); + int width = SXN_NUM_WIDTH(magic); + int64_t offset = 0; + if (argc > 0 && !JS_IsUndefined(argv[0]) && JS_ToInt64(ctx, &offset, argv[0])) return JS_EXCEPTION; + if (!sxn_num_range(ctx, len, offset, width)) return JS_EXCEPTION; + const uint8_t *p = bytes + offset; + bool big = (magic & SXN_NUM_BIG_END) != 0; + uint64_t raw = sxn_read_raw(p, width, big); + if (magic & SXN_NUM_FLOAT) { + if (width == 4) { float f; uint32_t bits = (uint32_t)raw; memcpy(&f, &bits, 4); return JS_NewFloat64(ctx, (double)f); } + double d; memcpy(&d, &raw, 8); return JS_NewFloat64(ctx, d); + } + if (magic & SXN_NUM_BIG_INT) { + if (magic & SXN_NUM_SIGNED) return JS_NewBigInt64(ctx, (int64_t)raw); + return JS_NewBigUint64(ctx, raw); + } + if (magic & SXN_NUM_SIGNED) { + int shift = 64 - width * 8; + return JS_NewInt64(ctx, ((int64_t)(raw << shift)) >> shift); + } + return JS_NewInt64(ctx, (int64_t)raw); +} + +static JSValue js_buffer_write_num(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, this_val); + if (!bytes) return JS_ThrowTypeError(ctx, "not a Buffer"); + int width = SXN_NUM_WIDTH(magic); + int64_t offset = 0; + if (argc > 1 && !JS_IsUndefined(argv[1]) && JS_ToInt64(ctx, &offset, argv[1])) return JS_EXCEPTION; + if (!sxn_num_range(ctx, len, offset, width)) return JS_EXCEPTION; + uint8_t *p = bytes + offset; + bool big = (magic & SXN_NUM_BIG_END) != 0; + if (magic & SXN_NUM_FLOAT) { + double d = 0; + if (JS_ToFloat64(ctx, &d, argc > 0 ? argv[0] : JS_UNDEFINED)) return JS_EXCEPTION; + if (width == 4) { float f = (float)d; uint32_t bits; memcpy(&bits, &f, 4); sxn_write_raw(p, bits, 4, big); } + else { uint64_t bits; memcpy(&bits, &d, 8); sxn_write_raw(p, bits, 8, big); } + } else if (magic & SXN_NUM_BIG_INT) { + int64_t v = 0; + if (JS_ToBigInt64(ctx, &v, argc > 0 ? argv[0] : JS_UNDEFINED)) return JS_EXCEPTION; + sxn_write_raw(p, (uint64_t)v, width, big); + } else { + double d = 0; + if (JS_ToFloat64(ctx, &d, argc > 0 ? argv[0] : JS_UNDEFINED)) return JS_EXCEPTION; + sxn_write_raw(p, (uint64_t)(int64_t)d, width, big); + } + return JS_NewInt64(ctx, offset + width); +} + +/* Buffer#copy(target, targetStart, sourceStart, sourceEnd) -> bytes copied, + with the overlapping case handled the way Node's is. */ +static JSValue js_buffer_copy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + size_t src_len = 0, dst_len = 0; + uint8_t *src = JS_GetUint8Array(ctx, &src_len, this_val); + uint8_t *dst = argc > 0 ? JS_GetUint8Array(ctx, &dst_len, argv[0]) : NULL; + if (!src || !dst) return JS_ThrowTypeError(ctx, "copy expects a Buffer target"); + int64_t target_start = 0, source_start = 0, source_end = (int64_t)src_len; + if (argc > 1 && !JS_IsUndefined(argv[1]) && JS_ToInt64(ctx, &target_start, argv[1])) return JS_EXCEPTION; + if (argc > 2 && !JS_IsUndefined(argv[2]) && JS_ToInt64(ctx, &source_start, argv[2])) return JS_EXCEPTION; + if (argc > 3 && !JS_IsUndefined(argv[3]) && JS_ToInt64(ctx, &source_end, argv[3])) return JS_EXCEPTION; + if (target_start < 0 || source_start < 0 || source_end < source_start) + return JS_ThrowRangeError(ctx, "index out of range"); + if (source_end > (int64_t)src_len) source_end = (int64_t)src_len; + int64_t n = source_end - source_start; + if (n > (int64_t)dst_len - target_start) n = (int64_t)dst_len - target_start; + if (n <= 0) return JS_NewInt32(ctx, 0); + memmove(dst + target_start, src + source_start, (size_t)n); + return JS_NewInt64(ctx, n); +} + + +/* The variable-width accessors: readUIntBE(offset, byteLength) and its + siblings take the width as an argument, 1 to 6 bytes, which is why they + cannot share the fixed-width table above. */ +static JSValue js_buffer_read_var(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, this_val); + if (!bytes) return JS_ThrowTypeError(ctx, "not a Buffer"); + int64_t offset = 0, width = 0; + if (argc > 0 && JS_ToInt64(ctx, &offset, argv[0])) return JS_EXCEPTION; + if (argc > 1 && JS_ToInt64(ctx, &width, argv[1])) return JS_EXCEPTION; + if (width < 1 || width > 6) return JS_ThrowRangeError(ctx, "byteLength must be between 1 and 6"); + if (!sxn_num_range(ctx, len, offset, (int)width)) return JS_EXCEPTION; + uint64_t raw = sxn_read_raw(bytes + offset, (int)width, (magic & SXN_NUM_BIG_END) != 0); + if (magic & SXN_NUM_SIGNED) { + int shift = 64 - (int)width * 8; + return JS_NewInt64(ctx, ((int64_t)(raw << shift)) >> shift); + } + return JS_NewInt64(ctx, (int64_t)raw); +} + +static JSValue js_buffer_write_var(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, this_val); + if (!bytes) return JS_ThrowTypeError(ctx, "not a Buffer"); + double value = 0; + int64_t offset = 0, width = 0; + if (argc > 0 && JS_ToFloat64(ctx, &value, argv[0])) return JS_EXCEPTION; + if (argc > 1 && JS_ToInt64(ctx, &offset, argv[1])) return JS_EXCEPTION; + if (argc > 2 && JS_ToInt64(ctx, &width, argv[2])) return JS_EXCEPTION; + if (width < 1 || width > 6) return JS_ThrowRangeError(ctx, "byteLength must be between 1 and 6"); + if (!sxn_num_range(ctx, len, offset, (int)width)) return JS_EXCEPTION; + sxn_write_raw(bytes + offset, (uint64_t)(int64_t)value, (int)width, (magic & SXN_NUM_BIG_END) != 0); + return JS_NewInt64(ctx, offset + width); +} + +/* swap16/swap32/swap64: reverse each group of bytes in place. */ +static JSValue js_buffer_swap(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + (void)argc; (void)argv; + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, this_val); + if (!bytes) return JS_ThrowTypeError(ctx, "not a Buffer"); + size_t width = (size_t)magic; + if (len % width) return JS_ThrowRangeError(ctx, "buffer size must be a multiple of %d", (int)width); + for (size_t i = 0; i + width <= len; i += width) + for (size_t a = 0, b = width - 1; a < b; a++, b--) { + uint8_t t = bytes[i + a]; + bytes[i + a] = bytes[i + b]; + bytes[i + b] = t; + } + return JS_DupValue(ctx, this_val); +} + + +/* Buffer#write(string, offset, length, encoding): UTF-8 into the bytes that + are already there, which is the shape a protocol writer wants and which + this runtime did not have at all. */ +static JSValue js_buffer_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + size_t buf_len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &buf_len, this_val); + if (!bytes) return JS_ThrowTypeError(ctx, "not a Buffer"); + if (argc < 1) return JS_NewInt32(ctx, 0); + + int64_t offset = 0, max = -1; + const char *encoding = NULL; + /* write(string), write(string, encoding), write(string, offset[, length][, encoding]) */ + int at = 1; + if (at < argc && JS_IsString(argv[at])) { + encoding = JS_ToCString(ctx, argv[at]); + at++; + } else { + if (at < argc && !JS_IsUndefined(argv[at])) { if (JS_ToInt64(ctx, &offset, argv[at])) return JS_EXCEPTION; } + at++; + if (at < argc && !JS_IsUndefined(argv[at]) && !JS_IsString(argv[at])) { if (JS_ToInt64(ctx, &max, argv[at])) return JS_EXCEPTION; at++; } + if (at < argc && JS_IsString(argv[at])) { encoding = JS_ToCString(ctx, argv[at]); at++; } + } + bool utf8 = !encoding || !strcmp(encoding, "utf8") || !strcmp(encoding, "utf-8"); + if (encoding) JS_FreeCString(ctx, encoding); + if (!utf8) return JS_ThrowTypeError(ctx, "Buffer#write supports utf-8 only"); + if (offset < 0 || (uint64_t)offset > (uint64_t)buf_len) + return JS_ThrowRangeError(ctx, "the value of \"offset\" is out of range"); + + size_t text_len = 0; + const char *text = JS_ToCStringLen(ctx, &text_len, argv[0]); + if (!text) return JS_EXCEPTION; + size_t room = buf_len - (size_t)offset; + if (max >= 0 && (size_t)max < room) room = (size_t)max; + size_t n = text_len < room ? text_len : room; + /* Never leave half a character behind: back off to a boundary. */ + while (n > 0 && n < text_len && ((unsigned char)text[n] & 0xc0) == 0x80) n--; + memcpy(bytes + offset, text, n); + JS_FreeCString(ctx, text); + return JS_NewInt64(ctx, (int64_t)n); +} + + +/* Node's lenient hex and base64 readers, which every real payload takes: + Uint8Array.fromHex and fromBase64 are strict and throw on the first + character they do not like, and base64 as it actually travels -- PEM, MIME + -- has newlines in it. + + Both are defined over UTF-16 code units: Node reads the string one unit at + a time and masks it to a byte, which is why an emoji ends a base64 string. + A C function is handed UTF-8 and cannot see that, so these return NULL for + anything non-ASCII and the caller keeps the JavaScript loop for it. Every + valid hex or base64 string is ASCII. */ +static void sxn_free_plain_buffer(JSRuntime *rt, void *opaque, void *ptr) { + (void)rt; (void)opaque; + free(ptr); +} + +static uint8_t *sxn_string_code_units(JSContext *ctx, JSValueConst val, size_t *out_count, bool wide); + +static JSValue js_hex_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_NULL; + size_t len = 0; + const char *str = JS_ToCStringLen(ctx, &len, argv[0]); + if (!str) return JS_EXCEPTION; + uint8_t *out = malloc(len / 2 + 1); + if (!out) { JS_FreeCString(ctx, str); return JS_ThrowOutOfMemory(ctx); } + size_t n = 0; + bool ascii = true; + for (size_t i = 0; i + 1 < len; i += 2) { + unsigned char a = (unsigned char)str[i], b = (unsigned char)str[i + 1]; + if ((a | b) & 0x80) { ascii = false; break; } + int hi = sxn_hex_value(a), lo = sxn_hex_value(b); + if (hi < 0 || lo < 0) break; /* Node stops here rather than throwing */ + out[n++] = (uint8_t)((hi << 4) | lo); + } + JS_FreeCString(ctx, str); + if (ascii) { + /* The buffer goes to JavaScript as it stands rather than being + copied into a fresh one. */ + return JS_NewUint8Array(ctx, out, n, sxn_free_plain_buffer, NULL, false); + } + /* Anything non-ASCII means Node's byte-at-a-time reading of the string + is visible, so read the code units and mask each to a byte. */ + free(out); + size_t count = 0; + uint8_t *units = sxn_string_code_units(ctx, argv[0], &count, false); + if (!units) return JS_EXCEPTION; + uint8_t *bytes = malloc(count / 2 + 1); + if (!bytes) { js_free(ctx, units); return JS_ThrowOutOfMemory(ctx); } + n = 0; + for (size_t i = 0; i + 1 < count; i += 2) { + int hi = sxn_hex_value(units[i]), lo = sxn_hex_value(units[i + 1]); + if (hi < 0 || lo < 0) break; + bytes[n++] = (uint8_t)((hi << 4) | lo); + } + js_free(ctx, units); + return JS_NewUint8Array(ctx, bytes, n, sxn_free_plain_buffer, NULL, false); +} + +static JSValue js_base64_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_NULL; + size_t len = 0; + const char *str = JS_ToCStringLen(ctx, &len, argv[0]); + if (!str) return JS_EXCEPTION; + /* Both alphabets at once, which is what Node's reader accepts. */ + static int8_t table[256]; + static bool built = false; + if (!built) { + for (int i = 0; i < 256; i++) table[i] = -1; + const char *a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (int i = 0; a[i]; i++) table[(unsigned char)a[i]] = (int8_t)i; + table[(unsigned char)'+'] = 62; table[(unsigned char)'/'] = 63; + table[(unsigned char)'-'] = 62; table[(unsigned char)'_'] = 63; + built = true; + } + uint8_t *out = malloc(len * 3 / 4 + 4); + if (!out) { JS_FreeCString(ctx, str); return JS_ThrowOutOfMemory(ctx); } + size_t n = 0; + uint32_t acc = 0; + int bits = 0; + bool ascii = true; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)str[i]; + if (c & 0x80) { ascii = false; break; } + if (c == '=') break; /* padding ends the data */ + int v = table[c]; + if (v < 0) continue; /* anything else is skipped */ + acc = (acc << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { bits -= 8; out[n++] = (uint8_t)((acc >> bits) & 0xff); } + } + JS_FreeCString(ctx, str); + if (ascii) return JS_NewUint8Array(ctx, out, n, sxn_free_plain_buffer, NULL, false); + /* Non-ASCII: Node reads the string a byte at a time, so a code unit + above 0xff is truncated rather than skipped -- which is why an emoji + ends a base64 string, its surrogate's low byte being '='. */ + free(out); + size_t count = 0; + uint8_t *units = sxn_string_code_units(ctx, argv[0], &count, false); + if (!units) return JS_EXCEPTION; + uint8_t *bytes = malloc(count * 3 / 4 + 4); + if (!bytes) { js_free(ctx, units); return JS_ThrowOutOfMemory(ctx); } + n = 0; acc = 0; bits = 0; + for (size_t i = 0; i < count; i++) { + uint8_t c = units[i]; + if (c == '=') break; + int v = table[c]; + if (v < 0) continue; + acc = (acc << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { bits -= 8; bytes[n++] = (uint8_t)((acc >> bits) & 0xff); } + } + js_free(ctx, units); + return JS_NewUint8Array(ctx, bytes, n, sxn_free_plain_buffer, NULL, false); +} + + +/* util.format, in C. The scan and the substitution are string work; only + the two cases that need util.inspect -- %s of something that is not a + string, and %o/%O -- call back into JavaScript, and the inspect function + is handed in for that. %j is JSON, which the engine already has. */ +static void sxn_format_append_value(JSContext *ctx, DynStr *out, JSValueConst value) { + size_t len = 0; + const char *text = JS_ToCStringLen(ctx, &len, value); + if (text) { dynstr_add(out, text, len); JS_FreeCString(ctx, text); } +} + +/* Calls the JavaScript inspect that was passed as the first argument. */ +static void sxn_format_inspect(JSContext *ctx, DynStr *out, JSValueConst inspect, JSValueConst value, int depth) { + JSValue args[2]; + args[0] = JS_DupValue(ctx, value); + if (depth >= 0) { + args[1] = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, args[1], "depth", JS_NewInt32(ctx, depth)); + } else { + args[1] = JS_UNDEFINED; + } + JSValue text = JS_Call(ctx, inspect, JS_UNDEFINED, depth >= 0 ? 2 : 1, (JSValueConst *)args); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + if (!JS_IsException(text)) sxn_format_append_value(ctx, out, text); + JS_FreeValue(ctx, text); +} + +static JSValue js_util_format(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_NewString(ctx, ""); + JSValueConst inspect = argv[0]; + int first = 1; + DynStr out = {0}; + /* Without a string to substitute into, everything is inspected. */ + if (argc <= first || !JS_IsString(argv[first])) { + for (int i = first; i < argc; i++) { + if (i > first) dynstr_add(&out, " ", 1); + sxn_format_inspect(ctx, &out, inspect, argv[i], -1); + } + JSValue result = JS_NewStringLen(ctx, out.data ? out.data : "", out.len); + free(out.data); + return result; + } + /* One argument: Node hands the string back untouched, "%%" included. */ + if (argc == first + 1) return JS_DupValue(ctx, argv[first]); + + size_t len = 0; + const char *fmt = JS_ToCStringLen(ctx, &len, argv[first]); + if (!fmt) return JS_EXCEPTION; + int next = first + 1; + size_t i = 0; + while (i < len) { + if (fmt[i] != '%' || i + 1 >= len) { dynstr_add(&out, fmt + i, 1); i++; continue; } + char kind = fmt[i + 1]; + if (kind == '%') { dynstr_add(&out, "%", 1); i += 2; continue; } + if (!strchr("sdifjoOc", kind) || next >= argc) { dynstr_add(&out, fmt + i, 1); i++; continue; } + JSValueConst value = argv[next++]; + i += 2; + switch (kind) { + case 's': + if (JS_IsString(value)) sxn_format_append_value(ctx, &out, value); + else sxn_format_inspect(ctx, &out, inspect, value, -1); + break; + case 'd': case 'f': { + if (JS_IsBigInt(value)) { sxn_format_append_value(ctx, &out, value); dynstr_add(&out, "n", 1); break; } + double d = 0; + if (JS_ToFloat64(ctx, &d, value)) { JS_FreeValue(ctx, JS_GetException(ctx)); d = NAN; } + JSValue number = JS_NewFloat64(ctx, d); + sxn_format_append_value(ctx, &out, number); + JS_FreeValue(ctx, number); + break; + } + case 'i': { + if (JS_IsBigInt(value)) { sxn_format_append_value(ctx, &out, value); dynstr_add(&out, "n", 1); break; } + double d = 0; + if (JS_ToFloat64(ctx, &d, value)) { JS_FreeValue(ctx, JS_GetException(ctx)); d = NAN; } + JSValue number = JS_NewFloat64(ctx, isnan(d) ? NAN : trunc(d)); + sxn_format_append_value(ctx, &out, number); + JS_FreeValue(ctx, number); + break; + } + case 'j': { + JSValue json = JS_JSONStringify(ctx, value, JS_UNDEFINED, JS_UNDEFINED); + if (JS_IsException(json)) { + JS_FreeValue(ctx, JS_GetException(ctx)); + dynstr_add(&out, "[Circular]", 10); + } else if (JS_IsUndefined(json)) { + dynstr_add(&out, "undefined", 9); + } else { + sxn_format_append_value(ctx, &out, json); + } + JS_FreeValue(ctx, json); + break; + } + case 'o': case 'O': + sxn_format_inspect(ctx, &out, inspect, value, 4); + break; + case 'c': + break; /* a CSS directive, which has nothing to say here */ + } + } + JS_FreeCString(ctx, fmt); + /* Whatever is left over follows, separated by spaces. */ + for (; next < argc; next++) { + dynstr_add(&out, " ", 1); + if (JS_IsString(argv[next])) sxn_format_append_value(ctx, &out, argv[next]); + else sxn_format_inspect(ctx, &out, inspect, argv[next], -1); + } + JSValue result = JS_NewStringLen(ctx, out.data ? out.data : "", out.len); + free(out.data); + return result; +} + + + +/* Buffer#toString for the encodings that are a straight widening of bytes + into code units: latin1, Node's 7-bit "ascii", and utf16le. In JavaScript + each of these was a String.fromCharCode appended in a loop, which builds a + new string per byte; here the code units are filled in once and handed to + the engine as a whole string. */ +static JSValue js_buffer_decode_units(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + (void)this_val; + size_t len = 0; + uint8_t *bytes = argc > 0 ? JS_GetUint8Array(ctx, &len, argv[0]) : NULL; + if (!bytes) return JS_EXCEPTION; + size_t count = magic == 2 ? len / 2 : len; + if (count == 0) return JS_NewStringLen(ctx, "", 0); + uint16_t *units = js_malloc(ctx, count * sizeof(uint16_t)); + if (!units) return JS_EXCEPTION; + if (magic == 2) + for (size_t i = 0; i < count; i++) units[i] = (uint16_t)(bytes[i * 2] | (bytes[i * 2 + 1] << 8)); + else { + uint8_t mask = magic == 1 ? 0x7f : 0xff; /* "ascii" drops the high bit */ + for (size_t i = 0; i < count; i++) units[i] = bytes[i] & mask; + } + JSValue str = JS_NewStringUTF16(ctx, units, count); + js_free(ctx, units); + return str; +} + + +/* A string's code units, each masked down to a byte -- what Node means by + latin1, and what its hex and base64 readers see in a string with anything + non-ASCII in it. The string is read as CESU-8, which encodes each + surrogate on its own, so every code unit survives the trip. */ +static uint8_t *sxn_string_code_units(JSContext *ctx, JSValueConst val, size_t *out_count, bool wide) { + size_t len = 0; + const char *str = JS_ToCStringLen2(ctx, &len, val, true); + if (!str) return NULL; + uint8_t *out = js_malloc(ctx, wide ? (len + 1) * 2 : len + 1); + if (!out) { JS_FreeCString(ctx, str); return NULL; } + size_t n = 0; + for (size_t i = 0; i < len; ) { + uint8_t c = (uint8_t)str[i]; + uint32_t unit; + if (c < 0x80) { unit = c; i += 1; } + else if ((c & 0xe0) == 0xc0 && i + 1 < len) { unit = ((c & 0x1fu) << 6) | ((uint8_t)str[i + 1] & 0x3fu); i += 2; } + else if ((c & 0xf0) == 0xe0 && i + 2 < len) { + unit = ((c & 0x0fu) << 12) | (((uint8_t)str[i + 1] & 0x3fu) << 6) | ((uint8_t)str[i + 2] & 0x3fu); + i += 3; + } else { unit = c; i += 1; } + if (wide) { out[n++] = unit & 0xff; out[n++] = (unit >> 8) & 0xff; } + else out[n++] = unit & 0xff; + } + JS_FreeCString(ctx, str); + *out_count = n; + return out; +} + +/* The other direction for Buffer: a string into latin1 bytes (Node keeps the + low byte of each code unit) or into utf16le. */ +static JSValue js_buffer_encode_units(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + (void)this_val; + if (argc < 1) return JS_EXCEPTION; + size_t n = 0; + uint8_t *out = sxn_string_code_units(ctx, argv[0], &n, magic != 0); + if (!out) return JS_EXCEPTION; + JSValue bytes = JS_NewUint8ArrayCopy(ctx, out, n); + js_free(ctx, out); + return bytes; +} + +/* require() of a builtin. This was a 25-entry object literal in JavaScript, + rebuilt on every single require() call before the name was even looked at; + here the table is static and the answer is one property read. */ +typedef struct { const char *name, *global, *sub; } SxnBuiltinEntry; +static const SxnBuiltinEntry sxn_builtin_table[] = { + { "events", "__sxnEventEmitter", NULL }, + { "path", "__sxnPath", NULL }, + { "process", "process", NULL }, + { "fs", "__sxnFs", NULL }, + { "fs/promises", "__sxnFsPromises", NULL }, + { "util", "__sxnUtil", NULL }, + { "os", "__sxnOs", NULL }, + { "url", "__sxnUrl", NULL }, + { "querystring", "__sxnQuerystring", NULL }, + { "assert", "__sxnAssert", NULL }, + { "assert/strict", "__sxnAssert", NULL }, + { "stream", "__sxnStream", NULL }, + { "stream/promises", "__sxnStream", "promises" }, + { "http", "__sxnHttp", NULL }, + { "tty", "__sxnTty", NULL }, + { "string_decoder", "__sxnStringDecoder", NULL }, + { "timers", "__sxnTimers", NULL }, + { "timers/promises", "__sxnTimers", "promises" }, + { "perf_hooks", "__sxnPerfHooks", NULL }, + { "module", "__sxnModule", NULL }, + { "zlib", "__sxnZlib", NULL }, + { "crypto", "__sxnCrypto", NULL }, + { "net", "__sxnNet", NULL }, + { "child_process", "__sxnChildProcess", NULL }, + { "dns", "__sxnDns", NULL }, + { "dns/promises", "__sxnDnsPromises", NULL }, + { "https", "__sxnHttps", NULL }, + { "tls", "__sxnTls", NULL }, + { "http2", "__sxnHttp2", NULL }, + { "stream/web", "__sxnStreamWeb", NULL }, + { "vm", "__sxnVm", NULL }, + { "v8", "__sxnV8", NULL }, + { "worker_threads", "__sxnWorkerThreads", NULL }, + { "cluster", "__sxnCluster", NULL }, + { "readline", "__sxnReadline", NULL }, + { "readline/promises", "__sxnReadlinePromises", NULL }, + { "async_hooks", "__sxnAsyncHooks", NULL }, + { "inspector", "__sxnInspector", NULL }, + { "dgram", "__sxnDgram", NULL }, + { "console", "__sxnConsole", NULL }, + { "constants", "__sxnConstants", NULL }, + { "punycode", "__sxnPunycode", NULL }, + { "diagnostics_channel", "__sxnDiagnosticsChannel", NULL }, + { NULL, NULL, NULL }, +}; + +/* module.builtinModules, read off the same table require() uses -- the list + used to be written out by hand in JavaScript and had fallen behind it. */ +static JSValue js_builtin_names(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + JSValue list = JS_NewArray(ctx); + uint32_t n = 0; + JS_SetPropertyUint32(ctx, list, n++, JS_NewString(ctx, "buffer")); + for (const SxnBuiltinEntry *e = sxn_builtin_table; e->name; e++) + JS_SetPropertyUint32(ctx, list, n++, JS_NewString(ctx, e->name)); + return list; +} + +/* Returns JS_UNINITIALIZED for a name that is not a builtin, so the caller + decides between throwing and answering false. */ +static JSValue sxn_builtin_lookup(JSContext *ctx, JSValueConst spec) { + const char *name = JS_ToCString(ctx, spec); + if (!name) return JS_EXCEPTION; + const char *bare = strncmp(name, "node:", 5) == 0 ? name + 5 : name; + JSValue global = JS_GetGlobalObject(ctx); + JSValue result = JS_UNINITIALIZED; + if (!strcmp(bare, "buffer")) { + /* node:buffer is the constructor under both names, built once. */ + result = JS_GetPropertyStr(ctx, global, "__sxnBufferModule"); + if (JS_IsUndefined(result)) { + JSValue buffer = JS_GetPropertyStr(ctx, global, "Buffer"); + result = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, result, "Buffer", JS_DupValue(ctx, buffer)); + JS_SetPropertyStr(ctx, result, "default", buffer); + JS_SetPropertyStr(ctx, global, "__sxnBufferModule", JS_DupValue(ctx, result)); + } + } else { + for (const SxnBuiltinEntry *e = sxn_builtin_table; e->name; e++) { + if (strcmp(bare, e->name)) continue; + result = JS_GetPropertyStr(ctx, global, e->global); + if (e->sub && !JS_IsUndefined(result) && !JS_IsNull(result)) { + JSValue outer = result; + result = JS_GetPropertyStr(ctx, outer, e->sub); + JS_FreeValue(ctx, outer); + } + if (JS_IsUndefined(result)) result = JS_NULL; /* known, not loaded */ + break; + } + } + JS_FreeValue(ctx, global); + JS_FreeCString(ctx, name); + return result; +} + +static JSValue js_builtin_require(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_ThrowTypeError(ctx, "require expects a specifier"); + JSValue mod = sxn_builtin_lookup(ctx, argv[0]); + if (JS_IsException(mod)) return mod; + if (JS_IsUninitialized(mod)) { + const char *name = JS_ToCString(ctx, argv[0]); + JSValue err = JS_NewError(ctx); + JS_SetPropertyStr(ctx, err, "message", JS_NewString(ctx, name ? name : "?")); + JS_SetPropertyStr(ctx, err, "code", JS_NewString(ctx, "MODULE_NOT_FOUND")); + if (name) { + char msg[256]; + snprintf(msg, sizeof msg, "Cannot find module '%s'", name); + JS_SetPropertyStr(ctx, err, "message", JS_NewString(ctx, msg)); + JS_FreeCString(ctx, name); + } + return JS_Throw(ctx, err); + } + return mod; +} + +static JSValue js_is_builtin(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_NewBool(ctx, false); + JSValue mod = sxn_builtin_lookup(ctx, argv[0]); + if (JS_IsException(mod)) return mod; + bool known = !JS_IsUninitialized(mod); + JS_FreeValue(ctx, mod); + return JS_NewBool(ctx, known); +} + + +/* node:http lowercases every request header name and then flattens the + result into rawHeaders, once per request. Both walks happen here in one + pass over the property names. */ +static JSValue js_http_headers(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + JSValue headers = JS_NewObject(ctx); + JSValue raw = JS_NewArray(ctx); + if (JS_IsException(headers) || JS_IsException(raw)) goto fail; + if (argc > 0 && JS_IsObject(argv[0])) { + JSPropertyEnum *keys = NULL; + uint32_t count = 0; + if (JS_GetOwnPropertyNames(ctx, &keys, &count, argv[0], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY)) + goto fail; + uint32_t written = 0; + for (uint32_t i = 0; i < count; i++) { + JSValue value = JS_GetProperty(ctx, argv[0], keys[i].atom); + const char *name = JS_AtomToCString(ctx, keys[i].atom); + if (JS_IsException(value) || !name) { + JS_FreeValue(ctx, value); + if (name) JS_FreeCString(ctx, name); + JS_FreePropertyEnum(ctx, keys, count); + goto fail; + } + size_t len = strlen(name); + char stack[64]; + char *lower = len < sizeof stack ? stack : js_malloc(ctx, len + 1); + if (!lower) { + JS_FreeValue(ctx, value); + JS_FreeCString(ctx, name); + JS_FreePropertyEnum(ctx, keys, count); + goto fail; + } + for (size_t j = 0; j < len; j++) { + char c = name[j]; + lower[j] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; + } + lower[len] = '\0'; + JS_SetPropertyStr(ctx, headers, lower, JS_DupValue(ctx, value)); + JS_SetPropertyUint32(ctx, raw, written++, JS_NewStringLen(ctx, lower, len)); + JS_SetPropertyUint32(ctx, raw, written++, value); + if (lower != stack) js_free(ctx, lower); + JS_FreeCString(ctx, name); + } + JS_FreePropertyEnum(ctx, keys, count); + } + JSValue out = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, out, "headers", headers); + JS_SetPropertyStr(ctx, out, "rawHeaders", raw); + return out; + fail: + JS_FreeValue(ctx, headers); + JS_FreeValue(ctx, raw); + return JS_EXCEPTION; +} + + +/* res.end() turns the chunks written to it into one body. The common cases + -- nothing written, one string, all bytes -- are answered here; a mix of + strings and bytes is handed back undefined for the JavaScript to join, + because concatenating strings is the engine's own job. */ +static JSValue js_join_chunks(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1 || !JS_IsArray(argv[0])) return JS_UNDEFINED; + int64_t count = 0; + if (JS_GetLength(ctx, argv[0], &count)) return JS_EXCEPTION; + if (count == 0) return JS_NewStringLen(ctx, "", 0); + if (count == 1) { + JSValue only = JS_GetPropertyUint32(ctx, argv[0], 0); + if (JS_IsString(only)) return only; + JS_FreeValue(ctx, only); + } + /* All bytes: total the lengths, then copy each one in. */ + size_t total = 0; + for (int64_t i = 0; i < count; i++) { + JSValue chunk = JS_GetPropertyUint32(ctx, argv[0], (uint32_t)i); + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, chunk); + JS_FreeValue(ctx, chunk); + if (!bytes) { JS_FreeValue(ctx, JS_GetException(ctx)); return JS_UNDEFINED; } + total += len; + } + uint8_t *out = js_malloc(ctx, total ? total : 1); + if (!out) return JS_EXCEPTION; + size_t at = 0; + for (int64_t i = 0; i < count; i++) { + JSValue chunk = JS_GetPropertyUint32(ctx, argv[0], (uint32_t)i); + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, chunk); + if (bytes && len) memcpy(out + at, bytes, len); + at += len; + JS_FreeValue(ctx, chunk); + } + JSValue body = JS_NewUint8ArrayCopy(ctx, out, total); + js_free(ctx, out); + return body; +} + + +/* EventEmitter#once. The JavaScript version allocated a closure that had to + name itself in order to remove itself; here the wrapper is a C function + carrying the emitter, the event name and the listener, plus one small + holder that is given the wrapper once it exists. */ +static JSValue sxn_once_fire(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)magic; + JSValue wrapper = JS_GetPropertyStr(ctx, data[3], "w"); + JSValue off = JS_GetPropertyStr(ctx, data[0], "off"); + JSValueConst off_args[2] = { data[1], wrapper }; + JS_FreeValue(ctx, JS_Call(ctx, off, data[0], 2, off_args)); + JS_FreeValue(ctx, off); + JS_FreeValue(ctx, wrapper); + return JS_Call(ctx, data[2], this_val, argc, argv); +} + +static JSValue js_ee_once(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + if (argc < 2 || !JS_IsFunction(ctx, argv[1])) + return JS_ThrowTypeError(ctx, "once expects an event name and a listener"); + JSValue holder = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(holder)) return holder; + JSValue data[4] = { JS_DupValue(ctx, this_val), JS_DupValue(ctx, argv[0]), + JS_DupValue(ctx, argv[1]), holder }; + JSValue wrapper = JS_NewCFunctionData(ctx, sxn_once_fire, 0, 0, 4, data); + for (int i = 0; i < 4; i++) JS_FreeValue(ctx, data[i]); + if (JS_IsException(wrapper)) return wrapper; + JS_SetPropertyStr(ctx, holder, "w", JS_DupValue(ctx, wrapper)); + /* Node exposes the original listener here, and removeListener(original) + finds the wrapper through it. */ + JS_SetPropertyStr(ctx, wrapper, "_original", JS_DupValue(ctx, argv[1])); + JSValue on = JS_GetPropertyStr(ctx, this_val, "on"); + JSValueConst on_args[2] = { argv[0], wrapper }; + JSValue result = JS_Call(ctx, on, this_val, 2, on_args); + JS_FreeValue(ctx, on); + JS_FreeValue(ctx, wrapper); + return result; +} + + +/* Every node:http request carries a socket, and building it in JavaScript + meant a fresh EventEmitter plus eleven properties -- three of them + functions -- per request. The shape is the same every time, so the + prototype is built once here and each request gets an object pointing at + it. */ +static JSValue sxn_socket_self(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)ctx; (void)argc; (void)argv; + return JS_DupValue(ctx, this_val); +} + +static JSValue sxn_socket_destroy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)argc; (void)argv; + JS_SetPropertyStr(ctx, this_val, "destroyed", JS_TRUE); + JS_SetPropertyStr(ctx, this_val, "readable", JS_FALSE); + JS_SetPropertyStr(ctx, this_val, "writable", JS_FALSE); + JSValue emit = JS_GetPropertyStr(ctx, this_val, "emit"); + JSValue name = JS_NewString(ctx, "close"); + JSValueConst args[1] = { name }; + JS_FreeValue(ctx, JS_Call(ctx, emit, this_val, 1, args)); + JS_FreeValue(ctx, name); + JS_FreeValue(ctx, emit); + return JS_DupValue(ctx, this_val); +} + +static JSValue js_http_socket(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + JSValue global = JS_GetGlobalObject(ctx); + JSValue proto = JS_GetPropertyStr(ctx, global, "__sxnSocketProto"); + if (JS_IsUndefined(proto)) { + JSValue ee = JS_GetPropertyStr(ctx, global, "__sxnEventEmitter"); + JSValue ee_proto = JS_GetPropertyStr(ctx, ee, "prototype"); + JS_FreeValue(ctx, ee); + proto = JS_NewObjectProto(ctx, ee_proto); + JS_FreeValue(ctx, ee_proto); + JS_SetPropertyStr(ctx, proto, "remoteAddress", JS_NewString(ctx, "127.0.0.1")); + JS_SetPropertyStr(ctx, proto, "remotePort", JS_NewInt32(ctx, 0)); + JS_SetPropertyStr(ctx, proto, "localAddress", JS_NewString(ctx, "127.0.0.1")); + JS_SetPropertyStr(ctx, proto, "encrypted", JS_FALSE); + JS_SetPropertyStr(ctx, proto, "readable", JS_TRUE); + JS_SetPropertyStr(ctx, proto, "writable", JS_TRUE); + JS_SetPropertyStr(ctx, proto, "destroyed", JS_FALSE); + JS_SetPropertyStr(ctx, proto, "setTimeout", JS_NewCFunction(ctx, sxn_socket_self, "setTimeout", 0)); + JS_SetPropertyStr(ctx, proto, "setNoDelay", JS_NewCFunction(ctx, sxn_socket_self, "setNoDelay", 0)); + JS_SetPropertyStr(ctx, proto, "setKeepAlive", JS_NewCFunction(ctx, sxn_socket_self, "setKeepAlive", 0)); + JS_SetPropertyStr(ctx, proto, "destroy", JS_NewCFunction(ctx, sxn_socket_destroy, "destroy", 0)); + JS_SetPropertyStr(ctx, proto, "end", JS_NewCFunction(ctx, sxn_socket_destroy, "end", 0)); + JS_SetPropertyStr(ctx, global, "__sxnSocketProto", JS_DupValue(ctx, proto)); + } + JSValue socket = JS_NewObjectProto(ctx, proto); + JS_FreeValue(ctx, proto); + JS_FreeValue(ctx, global); + if (JS_IsException(socket)) return socket; + JS_SetPropertyStr(ctx, socket, "_events", JS_NewObjectProto(ctx, JS_NULL)); + return socket; +} + + + +/* A request's body is pushed on the first read, not before -- body-parser + attaches its 'data' listener after the handler returns. That deferral was + a closure built per request over `sent` and `body`; here it is one shared + function reading the two fields off the request itself. */ +static JSValue js_http_read_body(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)argc; (void)argv; + JSValue sent = JS_GetPropertyStr(ctx, this_val, "_bodySent"); + bool already = JS_ToBool(ctx, sent); + JS_FreeValue(ctx, sent); + if (already) return JS_UNDEFINED; + JS_SetPropertyStr(ctx, this_val, "_bodySent", JS_TRUE); + JSValue body = JS_GetPropertyStr(ctx, this_val, "_rawBody"); + JSValue push = JS_GetPropertyStr(ctx, this_val, "push"); + bool empty = JS_IsUndefined(body) || JS_IsNull(body); + if (!empty && JS_IsString(body)) { + const char *text = JS_ToCString(ctx, body); + empty = text && text[0] == '\0'; + if (text) JS_FreeCString(ctx, text); + } + if (!empty) { + JSValueConst args[1] = { body }; + JS_FreeValue(ctx, JS_Call(ctx, push, this_val, 1, args)); + } + JS_FreeValue(ctx, body); + JSValueConst end[1] = { JS_NULL }; + JS_FreeValue(ctx, JS_Call(ctx, push, this_val, 1, end)); + JS_FreeValue(ctx, push); + return JS_UNDEFINED; +} + +/* The same for 'end' marking the request complete: one shared listener + instead of an arrow function per request. */ +static JSValue js_http_complete(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)argc; (void)argv; + JS_SetPropertyStr(ctx, this_val, "complete", JS_TRUE); + return JS_UNDEFINED; +} + + +/* Every one of res.setHeader/getHeader/hasHeader/removeHeader lowercases the + name it is given first. Scanning a short name in C beats + String(name).toLowerCase(): 0.17 microseconds a call became 0.058. */ +static JSValue js_header_op(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + if (argc < 1) return JS_DupValue(ctx, this_val); + const char *name = JS_ToCString(ctx, argv[0]); + if (!name) return JS_EXCEPTION; + size_t len = strlen(name); + char stack[64]; + char *lower = len < sizeof stack ? stack : js_malloc(ctx, len + 1); + if (!lower) { JS_FreeCString(ctx, name); return JS_EXCEPTION; } + for (size_t i = 0; i < len; i++) { + char c = name[i]; + lower[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; + } + lower[len] = '\0'; + JSValue headers = JS_GetPropertyStr(ctx, this_val, "_headers"); + JSValue result; + switch (magic) { + case 0: /* set */ + JS_SetPropertyStr(ctx, headers, lower, argc > 1 ? JS_DupValue(ctx, argv[1]) : JS_UNDEFINED); + result = JS_DupValue(ctx, this_val); + break; + case 1: /* get */ + result = JS_GetPropertyStr(ctx, headers, lower); + break; + case 2: { /* has: an own property, so a name like "constructor" is not one */ + JSAtom atom = JS_NewAtomLen(ctx, lower, len); + int has = JS_GetOwnProperty(ctx, NULL, headers, atom); + JS_FreeAtom(ctx, atom); + if (has < 0) { JS_FreeValue(ctx, headers); if (lower != stack) js_free(ctx, lower); JS_FreeCString(ctx, name); return JS_EXCEPTION; } + result = JS_NewBool(ctx, has > 0); + break; + } + default: /* remove */ + { + JSAtom atom = JS_NewAtomLen(ctx, lower, len); + JS_DeleteProperty(ctx, headers, atom, 0); + JS_FreeAtom(ctx, atom); + result = JS_UNDEFINED; + } + break; + } + JS_FreeValue(ctx, headers); + if (lower != stack) js_free(ctx, lower); + JS_FreeCString(ctx, name); + return result; +} + + + +/* Buffer.concat, and the same walk that node:crypto and node:zlib each had + a copy of: total the lengths, then copy each part in. Three JavaScript + loops became one memcpy per part. */ +static JSValue js_concat_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1 || !JS_IsArray(argv[0])) return JS_ThrowTypeError(ctx, "concat expects a list"); + int64_t count = 0; + if (JS_GetLength(ctx, argv[0], &count)) return JS_EXCEPTION; + size_t total = 0; + for (int64_t i = 0; i < count; i++) { + JSValue part = JS_GetPropertyUint32(ctx, argv[0], (uint32_t)i); + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, part); + JS_FreeValue(ctx, part); + if (!bytes) return JS_EXCEPTION; + total += len; + } + /* Node's second argument is the length to produce: short parts leave + zeroes behind them, long ones are cut off. An empty list is empty + whatever length was asked for, which is Node's own answer. */ + if (count > 0 && argc > 1 && !JS_IsUndefined(argv[1])) { + uint32_t wanted = 0; + if (JS_ToUint32(ctx, &wanted, argv[1])) return JS_EXCEPTION; + total = wanted; + } + uint8_t *out = js_mallocz(ctx, total ? total : 1); + if (!out) return JS_EXCEPTION; + size_t at = 0; + for (int64_t i = 0; i < count && at < total; i++) { + JSValue part = JS_GetPropertyUint32(ctx, argv[0], (uint32_t)i); + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, part); + if (bytes) { + if (len > total - at) len = total - at; + memcpy(out + at, bytes, len); + at += len; + } + JS_FreeValue(ctx, part); + } + JSValue result = JS_NewUint8ArrayCopy(ctx, out, total); + js_free(ctx, out); + return result; +} + + +/* The callback a Writable hands its _write. It was a JavaScript closure per + chunk; here it is a C function carrying the stream and the caller's + callback, if there was one. An error still reaches the stream's 'error' + listeners whether or not anybody passed a callback. */ +static JSValue sxn_write_done(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)this_val; (void)magic; + JSValueConst err = argc > 0 ? argv[0] : JS_UNDEFINED; + bool failed = !JS_IsUndefined(err) && !JS_IsNull(err); + if (failed) { + JSValue emit = JS_GetPropertyStr(ctx, data[0], "emit"); + JSValue name = JS_NewString(ctx, "error"); + JSValueConst args[2] = { name, err }; + JS_FreeValue(ctx, JS_Call(ctx, emit, data[0], 2, args)); + JS_FreeValue(ctx, name); + JS_FreeValue(ctx, emit); + } + if (JS_IsFunction(ctx, data[1])) { + JSValueConst args[1] = { failed ? err : JS_NULL }; + JS_FreeValue(ctx, JS_Call(ctx, data[1], JS_UNDEFINED, 1, args)); + } + return JS_UNDEFINED; +} + +static JSValue js_write_callback(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + JSValue data[2] = { JS_DupValue(ctx, argc > 0 ? argv[0] : JS_UNDEFINED), + JS_DupValue(ctx, argc > 1 ? argv[1] : JS_UNDEFINED) }; + JSValue fn = JS_NewCFunctionData(ctx, sxn_write_done, 1, 0, 2, data); + JS_FreeValue(ctx, data[0]); + JS_FreeValue(ctx, data[1]); + return fn; +} + + +/* url.fileURLToPath: strip the scheme and an empty "localhost" host, then + percent-decode what is left. This was a startsWith, a regexp and + decodeURIComponent per call. */ +static int sxn_hex_digit(unsigned char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static JSValue js_file_url_to_path(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_ThrowTypeError(ctx, "must be a file: URL"); + size_t len = 0; + const char *str = JS_ToCStringLen(ctx, &len, argv[0]); + if (!str) return JS_EXCEPTION; + if (len < 7 || memcmp(str, "file://", 7) != 0) { + JS_FreeCString(ctx, str); + return JS_ThrowTypeError(ctx, "must be a file: URL"); + } + const char *body = str + 7; + size_t body_len = len - 7; + if (body_len >= 9 && memcmp(body, "localhost", 9) == 0) { body += 9; body_len -= 9; } + char *out = js_malloc(ctx, body_len + 2); + if (!out) { JS_FreeCString(ctx, str); return JS_EXCEPTION; } + size_t n = 0; + for (size_t i = 0; i < body_len; i++) { + if (body[i] == '%' && i + 2 < body_len) { + int hi = sxn_hex_digit((unsigned char)body[i + 1]); + int lo = sxn_hex_digit((unsigned char)body[i + 2]); + if (hi >= 0 && lo >= 0) { out[n++] = (char)((hi << 4) | lo); i += 2; continue; } + } + out[n++] = body[i]; + } + if (n == 0) out[n++] = '/'; + JSValue path = JS_NewStringLen(ctx, out, n); + js_free(ctx, out); + JS_FreeCString(ctx, str); + return path; +} + + +/* The other direction: a path into the text of a file: URL. Node percent- + encodes what a URL cannot carry literally and leaves the rest alone; this + was encodeURI plus a regexp for '?' and '#' per call. */ +static JSValue js_path_to_file_url(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_ThrowTypeError(ctx, "pathToFileURL expects a path"); + size_t len = 0; + const char *path = JS_ToCStringLen(ctx, &len, argv[0]); + if (!path) return JS_EXCEPTION; + char *out = js_malloc(ctx, len * 3 + 8); + if (!out) { JS_FreeCString(ctx, path); return JS_EXCEPTION; } + static const char *hex = "0123456789ABCDEF"; + size_t n = 0; + memcpy(out, "file://", 7); + n = 7; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)path[i]; + /* encodeURI's unreserved set, less '?' and '#', which Node escapes + in a path because a URL would read them as query and fragment, + and less the brackets, which a URL keeps for IPv6 hosts. */ + bool literal = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || strchr("-_.!~*'();/:@&=+$,", (char)c) != NULL; + if (literal) out[n++] = (char)c; + else { out[n++] = '%'; out[n++] = hex[c >> 4]; out[n++] = hex[c & 0xf]; } + } + JSValue text = JS_NewStringLen(ctx, out, n); + js_free(ctx, out); + JS_FreeCString(ctx, path); + return text; +} + + +/* StringDecoder for utf-8: hand back everything up to the last complete + character and keep the incomplete tail for the next chunk. This went + through a TextDecoder with { stream: true }, which is the same idea at + four times the cost. The tail is at most three bytes, so it lives on the + decoder as a small array. */ +static JSValue js_decode_chunk(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 2) return JS_ThrowTypeError(ctx, "decodeChunk expects a decoder and bytes"); + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, argv[1]); + if (!bytes) return JS_EXCEPTION; + /* Whatever was left over last time comes first. */ + uint8_t tail[3]; + size_t tail_len = 0; + JSValue held = JS_GetPropertyStr(ctx, argv[0], "_tail"); + if (JS_IsObject(held)) { + size_t held_len = 0; + uint8_t *held_bytes = JS_GetUint8Array(ctx, &held_len, held); + if (held_bytes && held_len <= sizeof tail) { memcpy(tail, held_bytes, held_len); tail_len = held_len; } + else JS_FreeValue(ctx, JS_GetException(ctx)); + } + JS_FreeValue(ctx, held); + + size_t total = tail_len + len; + uint8_t *all = js_malloc(ctx, total ? total : 1); + if (!all) return JS_EXCEPTION; + if (tail_len) memcpy(all, tail, tail_len); + if (len) memcpy(all + tail_len, bytes, len); + + /* Walk back over at most three bytes looking for the start of a + sequence that has not all arrived yet. */ + size_t complete = total; + for (size_t back = 1; back <= 3 && back <= total; back++) { + uint8_t c = all[total - back]; + if ((c & 0xc0) == 0x80) continue; /* a continuation byte */ + size_t needed = (c & 0x80) == 0 ? 1 : (c & 0xe0) == 0xc0 ? 2 : (c & 0xf0) == 0xe0 ? 3 : (c & 0xf8) == 0xf0 ? 4 : 1; + if (needed > back) complete = total - back; /* short: hold it back */ + break; + } + JSValue text = JS_NewStringLen(ctx, (const char *)all, complete); + if (complete < total) { + JS_SetPropertyStr(ctx, argv[0], "_tail", JS_NewUint8ArrayCopy(ctx, all + complete, total - complete)); + } else { + JS_SetPropertyStr(ctx, argv[0], "_tail", JS_NULL); + } + js_free(ctx, all); + return text; +} + + +/* Measured against the JavaScript it would replace: res.getHeaders copies + the header object, res.getHeaderNames lists its keys. */ +static JSValue js_header_list(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + (void)argc; (void)argv; + JSValue headers = JS_GetPropertyStr(ctx, this_val, "_headers"); + JSPropertyEnum *keys = NULL; + uint32_t count = 0; + if (JS_GetOwnPropertyNames(ctx, &keys, &count, headers, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY)) { + JS_FreeValue(ctx, headers); + return JS_EXCEPTION; + } + JSValue out = magic ? JS_NewArray(ctx) : JS_NewObject(ctx); + for (uint32_t i = 0; i < count; i++) { + if (magic) JS_SetPropertyUint32(ctx, out, i, JS_AtomToString(ctx, keys[i].atom)); + else JS_SetProperty(ctx, out, keys[i].atom, JS_GetProperty(ctx, headers, keys[i].atom)); + } + JS_FreePropertyEnum(ctx, keys, count); + JS_FreeValue(ctx, headers); + return out; +} + + +#define SXN_TICK_ARRAY 4 /* magic is stored unsigned, so this is not -1 */ + +/* process.nextTick: the JavaScript version copied `arguments` into an array + and built a closure over it for every tick. The C one carries the + function and up to three arguments directly. */ +static JSValue sxn_tick_run(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)this_val; (void)argc; (void)argv; + if (magic == SXN_TICK_ARRAY) { + /* More than three arguments: they were kept as an array. */ + int64_t count = 0; + if (JS_GetLength(ctx, data[1], &count)) return JS_EXCEPTION; + JSValue *args = js_malloc(ctx, sizeof(JSValue) * (count ? (size_t)count : 1)); + if (!args) return JS_EXCEPTION; + for (int64_t i = 0; i < count; i++) args[i] = JS_GetPropertyUint32(ctx, data[1], (uint32_t)i); + JSValue out = JS_Call(ctx, data[0], JS_UNDEFINED, (int)count, (JSValueConst *)args); + for (int64_t i = 0; i < count; i++) JS_FreeValue(ctx, args[i]); + js_free(ctx, args); + return out; + } + JSValueConst args[3] = { data[1], data[2], data[3] }; + return JS_Call(ctx, data[0], JS_UNDEFINED, magic, args); +} + +static JSValue js_next_tick(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1 || !JS_IsFunction(ctx, argv[0])) + return JS_ThrowTypeError(ctx, "nextTick expects a function"); + int extra = argc - 1; + JSValue data[4] = { JS_DupValue(ctx, argv[0]), JS_UNDEFINED, JS_UNDEFINED, JS_UNDEFINED }; + int magic = extra; + if (extra > 3) { + /* Rare enough to keep in an array rather than widening the closure. */ + JSValue list = JS_NewArray(ctx); + for (int i = 0; i < extra; i++) + JS_SetPropertyUint32(ctx, list, (uint32_t)i, JS_DupValue(ctx, argv[i + 1])); + data[1] = list; + magic = SXN_TICK_ARRAY; + } else { + for (int i = 0; i < extra; i++) data[i + 1] = JS_DupValue(ctx, argv[i + 1]); + } + JSValue job = JS_NewCFunctionData(ctx, sxn_tick_run, 0, magic, 4, data); + for (int i = 0; i < 4; i++) JS_FreeValue(ctx, data[i]); + if (JS_IsException(job)) return job; + JSValue global = JS_GetGlobalObject(ctx); + JSValue enqueue = JS_GetPropertyStr(ctx, global, "queueMicrotask"); + JSValueConst call_args[1] = { job }; + JSValue result = JS_Call(ctx, enqueue, global, 1, call_args); + JS_FreeValue(ctx, enqueue); + JS_FreeValue(ctx, global); + JS_FreeValue(ctx, job); + if (JS_IsException(result)) return result; + JS_FreeValue(ctx, result); + return JS_UNDEFINED; +} + + +/* util.inspect. The JavaScript version built an options object per level, a + mapped array per container and a joined string per level; this walks the + value once into one buffer. What it prints is unchanged, down to the + quoting of keys that are not identifiers. */ +typedef struct SxnSeen { JSValueConst value; struct SxnSeen *prev; } SxnSeen; + +static void sxn_inspect_value(JSContext *ctx, DynStr *out, JSValueConst v, int depth, int max, SxnSeen *seen); + +static void sxn_inspect_str(JSContext *ctx, DynStr *out, JSValueConst v) { + size_t len = 0; + const char *text = JS_ToCStringLen(ctx, &len, v); + if (!text) { JS_FreeValue(ctx, JS_GetException(ctx)); return; } + dynstr_add(out, text, len); + JS_FreeCString(ctx, text); +} + +/* A string prints as JSON does inside a container, which is also how Node + quotes it -- except that Node uses single quotes. */ +static void sxn_inspect_quoted(JSContext *ctx, DynStr *out, JSValueConst v) { + JSValue global = JS_GetGlobalObject(ctx); + JSValue json = JS_GetPropertyStr(ctx, global, "JSON"); + JSValue stringify = JS_GetPropertyStr(ctx, json, "stringify"); + JSValueConst args[1] = { v }; + JSValue text = JS_Call(ctx, stringify, json, 1, args); + JS_FreeValue(ctx, stringify); + JS_FreeValue(ctx, json); + JS_FreeValue(ctx, global); + if (!JS_IsException(text)) sxn_inspect_str(ctx, out, text); + else JS_FreeValue(ctx, JS_GetException(ctx)); + JS_FreeValue(ctx, text); +} + +static bool sxn_is_identifier(const char *name, size_t len) { + if (len == 0) return false; + char c = name[0]; + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '$')) return false; + for (size_t i = 1; i < len; i++) { + c = name[i]; + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '$')) + return false; + } + return true; +} + +static void sxn_inspect_call(JSContext *ctx, DynStr *out, JSValueConst v, const char *method) { + JSValue fn = JS_GetPropertyStr(ctx, v, method); + JSValue text = JS_Call(ctx, fn, v, 0, NULL); + JS_FreeValue(ctx, fn); + if (!JS_IsException(text)) sxn_inspect_str(ctx, out, text); + else JS_FreeValue(ctx, JS_GetException(ctx)); + JS_FreeValue(ctx, text); +} + +static void sxn_inspect_entries(JSContext *ctx, DynStr *out, JSValueConst v, bool is_map, + int depth, int max, SxnSeen *seen) { + JSValue fn = JS_GetPropertyStr(ctx, v, is_map ? "entries" : "values"); + JSValue iterator = JS_Call(ctx, fn, v, 0, NULL); + JS_FreeValue(ctx, fn); + if (JS_IsException(iterator)) { JS_FreeValue(ctx, JS_GetException(ctx)); return; } + JSValue next = JS_GetPropertyStr(ctx, iterator, "next"); + bool first = true; + for (;;) { + JSValue step = JS_Call(ctx, next, iterator, 0, NULL); + if (JS_IsException(step)) { JS_FreeValue(ctx, JS_GetException(ctx)); break; } + JSValue done = JS_GetPropertyStr(ctx, step, "done"); + bool finished = JS_ToBool(ctx, done); + JS_FreeValue(ctx, done); + if (finished) { JS_FreeValue(ctx, step); break; } + JSValue entry = JS_GetPropertyStr(ctx, step, "value"); + JS_FreeValue(ctx, step); + if (!first) dynstr_add(out, ", ", 2); + first = false; + if (is_map) { + JSValue key = JS_GetPropertyUint32(ctx, entry, 0); + JSValue val = JS_GetPropertyUint32(ctx, entry, 1); + sxn_inspect_value(ctx, out, key, depth + 1, max, seen); + dynstr_add(out, " => ", 4); + sxn_inspect_value(ctx, out, val, depth + 1, max, seen); + JS_FreeValue(ctx, key); + JS_FreeValue(ctx, val); + } else { + sxn_inspect_value(ctx, out, entry, depth + 1, max, seen); + } + JS_FreeValue(ctx, entry); + } + JS_FreeValue(ctx, next); + JS_FreeValue(ctx, iterator); +} + +static void sxn_inspect_value(JSContext *ctx, DynStr *out, JSValueConst v, int depth, int max, SxnSeen *seen) { + if (JS_IsNull(v)) { dynstr_add(out, "null", 4); return; } + if (JS_IsUndefined(v)) { dynstr_add(out, "undefined", 9); return; } + if (JS_IsString(v)) { + if (depth == 0) sxn_inspect_str(ctx, out, v); + else sxn_inspect_quoted(ctx, out, v); + return; + } + if (JS_IsBool(v) || JS_IsNumber(v)) { sxn_inspect_str(ctx, out, v); return; } + if (JS_IsBigInt(v)) { sxn_inspect_str(ctx, out, v); dynstr_add(out, "n", 1); return; } + if (JS_IsSymbol(v)) { + /* A symbol refuses to become a string implicitly, so call its own + toString the way the JavaScript version did. */ + JSValue fn = JS_GetPropertyStr(ctx, v, "toString"); + JSValue text = JS_Call(ctx, fn, v, 0, NULL); + JS_FreeValue(ctx, fn); + if (JS_IsException(text)) { JS_FreeValue(ctx, JS_GetException(ctx)); dynstr_add(out, "Symbol()", 8); } + else sxn_inspect_str(ctx, out, text); + JS_FreeValue(ctx, text); + return; + } + if (JS_IsFunction(ctx, v)) { + JSValue name = JS_GetPropertyStr(ctx, v, "name"); + const char *text = JS_IsString(name) ? JS_ToCString(ctx, name) : NULL; + dynstr_add(out, "[Function: ", 11); + if (text && text[0]) dynstr_add(out, text, strlen(text)); + else dynstr_add(out, "anonymous", 9); + dynstr_add(out, "]", 1); + if (text) JS_FreeCString(ctx, text); + JS_FreeValue(ctx, name); + return; + } + if (!JS_IsObject(v)) { sxn_inspect_str(ctx, out, v); return; } + + JSValue global = JS_GetGlobalObject(ctx); + JSValue ctor_error = JS_GetPropertyStr(ctx, global, "Error"); + bool is_error = JS_IsInstanceOf(ctx, v, ctor_error) > 0; + JS_FreeValue(ctx, ctor_error); + JS_FreeValue(ctx, global); + if (is_error) { + /* This engine's stack is the frames alone, with no "Error: message" + line at the top of it, so that line is written here -- which is + what Node prints and what the JavaScript version left out. */ + JSValue name = JS_GetPropertyStr(ctx, v, "name"); + JSValue message = JS_GetPropertyStr(ctx, v, "message"); + sxn_inspect_str(ctx, out, name); + size_t message_len = 0; + const char *message_text = JS_ToCStringLen(ctx, &message_len, message); + if (message_text && message_len) { + dynstr_add(out, ": ", 2); + dynstr_add(out, message_text, message_len); + } + if (message_text) JS_FreeCString(ctx, message_text); + JS_FreeValue(ctx, name); + JS_FreeValue(ctx, message); + JSValue stack = JS_GetPropertyStr(ctx, v, "stack"); + size_t stack_len = 0; + const char *stack_text = JS_IsString(stack) ? JS_ToCStringLen(ctx, &stack_len, stack) : NULL; + if (stack_text && stack_len) { + if (stack_text[0] != '\n') dynstr_add(out, "\n", 1); + dynstr_add(out, stack_text, stack_len); + while (out->len && out->data[out->len - 1] == '\n') out->len--; + } + if (stack_text) JS_FreeCString(ctx, stack_text); + JS_FreeValue(ctx, stack); + return; + } + + static JSClassID date_id, regexp_id, map_id, set_id; + if (!date_id) { + static const char *probe_src = "[new Date(), /x/, new Map(), new Set()]"; + JSValue probe = JS_Eval(ctx, probe_src, strlen(probe_src), "", JS_EVAL_TYPE_GLOBAL); + JSValue item; + item = JS_GetPropertyUint32(ctx, probe, 0); date_id = JS_GetClassID(item); JS_FreeValue(ctx, item); + item = JS_GetPropertyUint32(ctx, probe, 1); regexp_id = JS_GetClassID(item); JS_FreeValue(ctx, item); + item = JS_GetPropertyUint32(ctx, probe, 2); map_id = JS_GetClassID(item); JS_FreeValue(ctx, item); + item = JS_GetPropertyUint32(ctx, probe, 3); set_id = JS_GetClassID(item); JS_FreeValue(ctx, item); + JS_FreeValue(ctx, probe); + } + JSClassID cls = JS_GetClassID(v); + if (cls == date_id) { + /* toISOString throws on an invalid date, which is what the + JavaScript version did to whoever printed one. Node prints + "Invalid Date" instead, and so does this. */ + JSValue fn = JS_GetPropertyStr(ctx, v, "toISOString"); + JSValue text = JS_Call(ctx, fn, v, 0, NULL); + JS_FreeValue(ctx, fn); + if (JS_IsException(text)) { JS_FreeValue(ctx, JS_GetException(ctx)); dynstr_add(out, "Invalid Date", 12); } + else sxn_inspect_str(ctx, out, text); + JS_FreeValue(ctx, text); + return; + } + if (cls == regexp_id) { sxn_inspect_call(ctx, out, v, "toString"); return; } + + for (SxnSeen *p = seen; p; p = p->prev) + if (JS_IsStrictEqual(ctx, p->value, v)) { dynstr_add(out, "[Circular *1]", 13); return; } + bool is_array = JS_IsArray(v); + if (depth > max) { + if (is_array) dynstr_add(out, "[Array]", 7); + else dynstr_add(out, "[Object]", 8); + return; + } + SxnSeen here = { v, seen }; + + if (is_array) { + int64_t count = 0; + JS_GetLength(ctx, v, &count); + if (count == 0) { dynstr_add(out, "[]", 2); return; } + dynstr_add(out, "[ ", 2); + for (int64_t i = 0; i < count; i++) { + if (i) dynstr_add(out, ", ", 2); + JSValue item = JS_GetPropertyUint32(ctx, v, (uint32_t)i); + sxn_inspect_value(ctx, out, item, depth + 1, max, &here); + JS_FreeValue(ctx, item); + } + dynstr_add(out, " ]", 2); + return; + } + if (cls == map_id || cls == set_id) { + bool is_map = cls == map_id; + JSValue size = JS_GetPropertyStr(ctx, v, "size"); + int32_t count = 0; + JS_ToInt32(ctx, &count, size); + JS_FreeValue(ctx, size); + char head[32]; + int head_len = snprintf(head, sizeof head, "%s(%d) {", is_map ? "Map" : "Set", count); + dynstr_add(out, head, (size_t)head_len); + if (count) { + dynstr_add(out, " ", 1); + sxn_inspect_entries(ctx, out, v, is_map, depth, max, &here); + dynstr_add(out, " ", 1); + } + dynstr_add(out, "}", 1); + return; + } + size_t ta_offset = 0, ta_len = 0, ta_element = 0; + JSValue ta_buffer = JS_GetTypedArrayBuffer(ctx, v, &ta_offset, &ta_len, &ta_element); + if (!JS_IsException(ta_buffer)) { + JS_FreeValue(ctx, ta_buffer); + /* A typed array prints its own class name and its elements. */ + JSValue ctor = JS_GetPropertyStr(ctx, v, "constructor"); + JSValue name = JS_GetPropertyStr(ctx, ctor, "name"); + JS_FreeValue(ctx, ctor); + int64_t count = 0; + JS_GetLength(ctx, v, &count); + sxn_inspect_str(ctx, out, name); + JS_FreeValue(ctx, name); + char head[32]; + int head_len = snprintf(head, sizeof head, "(%lld) [ ", (long long)count); + dynstr_add(out, head, (size_t)head_len); + for (int64_t i = 0; i < count; i++) { + if (i) dynstr_add(out, ", ", 2); + JSValue item = JS_GetPropertyUint32(ctx, v, (uint32_t)i); + sxn_inspect_str(ctx, out, item); + JS_FreeValue(ctx, item); + } + dynstr_add(out, " ]", 2); + return; + } + JS_FreeValue(ctx, JS_GetException(ctx)); /* not a typed array, then */ + + JSPropertyEnum *keys = NULL; + uint32_t count = 0; + if (JS_GetOwnPropertyNames(ctx, &keys, &count, v, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY)) { + JS_FreeValue(ctx, JS_GetException(ctx)); + dynstr_add(out, "{}", 2); + return; + } + if (count == 0) { JS_FreePropertyEnum(ctx, keys, count); dynstr_add(out, "{}", 2); return; } + dynstr_add(out, "{ ", 2); + for (uint32_t i = 0; i < count; i++) { + if (i) dynstr_add(out, ", ", 2); + JSValue key = JS_AtomToString(ctx, keys[i].atom); + size_t name_len = 0; + const char *name = JS_ToCStringLen(ctx, &name_len, key); + if (name && sxn_is_identifier(name, name_len)) dynstr_add(out, name, name_len); + else sxn_inspect_quoted(ctx, out, key); + if (name) JS_FreeCString(ctx, name); + JS_FreeValue(ctx, key); + dynstr_add(out, ": ", 2); + JSValue item = JS_GetProperty(ctx, v, keys[i].atom); + sxn_inspect_value(ctx, out, item, depth + 1, max, &here); + JS_FreeValue(ctx, item); + } + JS_FreePropertyEnum(ctx, keys, count); + dynstr_add(out, " }", 2); +} + +static JSValue js_inspect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + int max = 2; + bool quote_top = false; + if (argc > 1 && JS_IsObject(argv[1])) { + JSValue depth = JS_GetPropertyStr(ctx, argv[1], "depth"); + if (!JS_IsUndefined(depth)) { int32_t d = 2; JS_ToInt32(ctx, &d, depth); max = d; } + JS_FreeValue(ctx, depth); + JSValue quoted = JS_GetPropertyStr(ctx, argv[1], "quoteStrings"); + quote_top = JS_ToBool(ctx, quoted); + JS_FreeValue(ctx, quoted); + } + DynStr out = { 0 }; + sxn_inspect_value(ctx, &out, argc > 0 ? argv[0] : JS_UNDEFINED, quote_top ? 1 : 0, max, NULL); + JSValue text = JS_NewStringLen(ctx, out.data ? out.data : "", out.len); + free(out.data); + return text; +} + + +/* util.promisify. The JavaScript version spread the arguments, built a + Promise with an executor closure and then a callback closure inside it, + all per call. Here the promise is made directly and the callback is a C + function carrying its two resolving functions. */ +static JSValue sxn_promisify_callback(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)this_val; (void)magic; + JSValueConst err = argc > 0 ? argv[0] : JS_UNDEFINED; + if (!JS_IsUndefined(err) && !JS_IsNull(err)) { + JSValueConst args[1] = { err }; + JS_FreeValue(ctx, JS_Call(ctx, data[1], JS_UNDEFINED, 1, args)); + return JS_UNDEFINED; + } + /* Node resolves with the callback's first value and drops the rest, + which the JavaScript version here did not: it handed back an array. + Node's own answer is the one to keep. */ + JSValue value = argc > 1 ? JS_DupValue(ctx, argv[1]) : JS_UNDEFINED; + JSValueConst args[1] = { value }; + JS_FreeValue(ctx, JS_Call(ctx, data[0], JS_UNDEFINED, 1, args)); + JS_FreeValue(ctx, value); + return JS_UNDEFINED; +} + +static JSValue sxn_promisified(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)magic; + JSValue resolving[2]; + JSValue promise = JS_NewPromiseCapability(ctx, resolving); + if (JS_IsException(promise)) return promise; + JSValue callback = JS_NewCFunctionData(ctx, sxn_promisify_callback, 2, 0, 2, resolving); + if (JS_IsException(callback)) { + JS_FreeValue(ctx, resolving[0]); + JS_FreeValue(ctx, resolving[1]); + JS_FreeValue(ctx, promise); + return callback; + } + + JSValue *args = js_malloc(ctx, sizeof(JSValue) * (size_t)(argc + 1)); + if (!args) { JS_FreeValue(ctx, callback); JS_FreeValue(ctx, promise); return JS_EXCEPTION; } + for (int i = 0; i < argc; i++) args[i] = JS_DupValue(ctx, argv[i]); + args[argc] = callback; + JSValue result = JS_Call(ctx, data[0], this_val, argc + 1, (JSValueConst *)args); + for (int i = 0; i <= argc; i++) JS_FreeValue(ctx, args[i]); + js_free(ctx, args); + if (JS_IsException(result)) { + /* Node calls the function inside the promise, so a synchronous + throw comes back as a rejection rather than out of the call. */ + JSValue error = JS_GetException(ctx); + JSValueConst reject_args[1] = { error }; + JS_FreeValue(ctx, JS_Call(ctx, resolving[1], JS_UNDEFINED, 1, reject_args)); + JS_FreeValue(ctx, error); + } else { + JS_FreeValue(ctx, result); + } + JS_FreeValue(ctx, resolving[0]); + JS_FreeValue(ctx, resolving[1]); + return promise; +} + +static JSValue js_promisify(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1 || !JS_IsFunction(ctx, argv[0])) + return JS_ThrowTypeError(ctx, "promisify expects a function"); + JSValue data[1] = { JS_DupValue(ctx, argv[0]) }; + JSValue wrapped = JS_NewCFunctionData(ctx, sxn_promisified, 0, 0, 1, data); + JS_FreeValue(ctx, data[0]); + if (JS_IsException(wrapped)) return wrapped; + /* Node keeps the original's name on the wrapper. */ + JSValue name = JS_GetPropertyStr(ctx, argv[0], "name"); + JSAtom name_atom = JS_NewAtom(ctx, "name"); + JS_DefinePropertyValue(ctx, wrapped, name_atom, name, JS_PROP_CONFIGURABLE); + JS_FreeAtom(ctx, name_atom); + return wrapped; +} + + +/* Readable#pipe. Four closures per pipe in JavaScript, one for each of the + events involved; here they are four C functions sharing the same two + values -- the source and the destination -- and the fourth carries the + end option too. */ +static JSValue sxn_pipe_handler(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)this_val; + JSValueConst source = data[0], dest = data[1]; + switch (magic) { + case 0: { /* data: write, and pause the source if it says to */ + JSValue write = JS_GetPropertyStr(ctx, dest, "write"); + JSValueConst args[1] = { argc > 0 ? argv[0] : JS_UNDEFINED }; + JSValue ok = JS_Call(ctx, write, dest, 1, args); + JS_FreeValue(ctx, write); + if (JS_IsException(ok)) return ok; + bool full = JS_IsBool(ok) && !JS_ToBool(ctx, ok); + JS_FreeValue(ctx, ok); + if (full) { + JSValue pause = JS_GetPropertyStr(ctx, source, "pause"); + JS_FreeValue(ctx, JS_Call(ctx, pause, source, 0, NULL)); + JS_FreeValue(ctx, pause); + } + return JS_UNDEFINED; + } + case 1: { /* drain: the destination wants more */ + JSValue resume = JS_GetPropertyStr(ctx, source, "resume"); + JS_FreeValue(ctx, JS_Call(ctx, resume, source, 0, NULL)); + JS_FreeValue(ctx, resume); + return JS_UNDEFINED; + } + case 2: { /* end: unless the caller asked for the destination to stay */ + if (JS_ToBool(ctx, data[2])) { + JSValue end = JS_GetPropertyStr(ctx, dest, "end"); + JS_FreeValue(ctx, JS_Call(ctx, end, dest, 0, NULL)); + JS_FreeValue(ctx, end); + } + return JS_UNDEFINED; + } + default: { /* error: forward it */ + JSValue emit = JS_GetPropertyStr(ctx, dest, "emit"); + JSValue name = JS_NewString(ctx, "error"); + JSValueConst args[2] = { name, argc > 0 ? argv[0] : JS_UNDEFINED }; + JS_FreeValue(ctx, JS_Call(ctx, emit, dest, 2, args)); + JS_FreeValue(ctx, name); + JS_FreeValue(ctx, emit); + return JS_UNDEFINED; + } + } +} + +static void sxn_pipe_listen(JSContext *ctx, JSValueConst target, const char *event, JSValueConst fn) { + JSValue on = JS_GetPropertyStr(ctx, target, "on"); + JSValue name = JS_NewString(ctx, event); + JSValueConst args[2] = { name, fn }; + JS_FreeValue(ctx, JS_Call(ctx, on, target, 2, args)); + JS_FreeValue(ctx, name); + JS_FreeValue(ctx, on); +} + +static JSValue js_stream_pipe(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + if (argc < 1 || !JS_IsObject(argv[0])) return JS_ThrowTypeError(ctx, "pipe expects a destination"); + JSValueConst dest = argv[0]; + bool end_dest = true; + if (argc > 1 && JS_IsObject(argv[1])) { + JSValue end_opt = JS_GetPropertyStr(ctx, argv[1], "end"); + if (!JS_IsUndefined(end_opt)) end_dest = JS_ToBool(ctx, end_opt); + JS_FreeValue(ctx, end_opt); + } + JSValue data[3] = { JS_DupValue(ctx, this_val), JS_DupValue(ctx, dest), JS_NewBool(ctx, end_dest) }; + JSValue on_data = JS_NewCFunctionData(ctx, sxn_pipe_handler, 1, 0, 3, data); + JSValue on_drain = JS_NewCFunctionData(ctx, sxn_pipe_handler, 0, 1, 3, data); + JSValue on_end = JS_NewCFunctionData(ctx, sxn_pipe_handler, 0, 2, 3, data); + JSValue on_error = JS_NewCFunctionData(ctx, sxn_pipe_handler, 1, 3, 3, data); + for (int i = 0; i < 3; i++) JS_FreeValue(ctx, data[i]); + + sxn_pipe_listen(ctx, this_val, "data", on_data); + sxn_pipe_listen(ctx, dest, "drain", on_drain); + sxn_pipe_listen(ctx, this_val, "end", on_end); + sxn_pipe_listen(ctx, this_val, "error", on_error); + + /* unpipe needs to find these again, so the record is the same shape the + JavaScript kept. */ + JSValue record = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, record, "dest", JS_DupValue(ctx, dest)); + JS_SetPropertyStr(ctx, record, "onData", on_data); + JS_SetPropertyStr(ctx, record, "onDrain", on_drain); + JS_SetPropertyStr(ctx, record, "onEnd", on_end); + JS_SetPropertyStr(ctx, record, "onError", on_error); + JSValue pipes = JS_GetPropertyStr(ctx, this_val, "_pipes"); + if (!JS_IsObject(pipes)) { + JS_FreeValue(ctx, pipes); + pipes = JS_NewArray(ctx); + JS_SetPropertyStr(ctx, this_val, "_pipes", JS_DupValue(ctx, pipes)); + } + int64_t count = 0; + JS_GetLength(ctx, pipes, &count); + JS_SetPropertyUint32(ctx, pipes, (uint32_t)count, record); + JS_FreeValue(ctx, pipes); + + JSValue resume = JS_GetPropertyStr(ctx, this_val, "resume"); + JS_FreeValue(ctx, JS_Call(ctx, resume, this_val, 0, NULL)); + JS_FreeValue(ctx, resume); + return JS_DupValue(ctx, dest); +} + + +/* Buffer.from of a view. Going through `new Buffer(...)` meant running a + Uint8Array subclass constructor per call; the copy is made here and given + Buffer's prototype -- a fresh object of ours, so nothing the caller holds + is changed. The array-like path below is kept for a Float64Array and + friends, whose elements Node truncates to bytes one by one, but a plain + Array is left to the constructor: reading its elements from C measured + three times slower than the engine filling the array itself. */ +static JSValue js_buffer_from_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_ThrowTypeError(ctx, "Buffer.from: unsupported argument"); + size_t len = 0; + uint8_t *bytes = JS_GetUint8Array(ctx, &len, argv[0]); + JSValue out; + if (bytes) { + out = JS_NewUint8ArrayCopy(ctx, bytes, len); + } else { + JS_FreeValue(ctx, JS_GetException(ctx)); + int64_t count = 0; + if (JS_GetLength(ctx, argv[0], &count)) return JS_EXCEPTION; + uint8_t *raw = js_malloc(ctx, count ? (size_t)count : 1); + if (!raw) return JS_EXCEPTION; + for (int64_t i = 0; i < count; i++) { + JSValue item = JS_GetPropertyUint32(ctx, argv[0], (uint32_t)i); + int32_t value = 0; + /* Node truncates to a byte, and anything not a number is zero. */ + if (JS_ToInt32(ctx, &value, item)) { JS_FreeValue(ctx, JS_GetException(ctx)); value = 0; } + JS_FreeValue(ctx, item); + raw[i] = (uint8_t)value; + } + out = JS_NewUint8ArrayCopy(ctx, raw, (size_t)count); + js_free(ctx, raw); + } + if (JS_IsException(out)) return out; + JSValue global = JS_GetGlobalObject(ctx); + JSValue buffer_class = JS_GetPropertyStr(ctx, global, "Buffer"); + JSValue proto = JS_GetPropertyStr(ctx, buffer_class, "prototype"); + JS_FreeValue(ctx, buffer_class); + JS_FreeValue(ctx, global); + JS_SetPrototype(ctx, out, proto); + JS_FreeValue(ctx, proto); + return out; +} + + +/* util.inherits: two property operations, done from C instead of through + Object.defineProperty and Object.setPrototypeOf. */ +static JSValue js_inherits(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 2 || !JS_IsFunction(ctx, argv[0]) || !JS_IsFunction(ctx, argv[1])) + return JS_ThrowTypeError(ctx, "inherits expects two constructors"); + JSAtom super_atom = JS_NewAtom(ctx, "super_"); + JS_DefinePropertyValue(ctx, argv[0], super_atom, JS_DupValue(ctx, argv[1]), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_FreeAtom(ctx, super_atom); + JSValue proto = JS_GetPropertyStr(ctx, argv[0], "prototype"); + JSValue super_proto = JS_GetPropertyStr(ctx, argv[1], "prototype"); + JS_SetPrototype(ctx, proto, super_proto); + JS_FreeValue(ctx, proto); + JS_FreeValue(ctx, super_proto); + return JS_UNDEFINED; +} + + +/* util.callbackify: the JavaScript built two closures per call for the two + halves of the promise. Here they are C functions carrying the callback. */ +static JSValue sxn_callbackify_settle(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)this_val; + JSValueConst value = argc > 0 ? argv[0] : JS_UNDEFINED; + JSValue args[2]; + if (magic) { /* rejected */ + args[0] = JS_DupValue(ctx, value); + if (JS_IsNull(args[0]) || JS_IsUndefined(args[0])) { + JS_FreeValue(ctx, args[0]); + /* Node wraps a falsy rejection in an Error and keeps the + original on `reason`, which is worth having. */ + args[0] = JS_NewError(ctx); + JS_SetPropertyStr(ctx, args[0], "message", + JS_NewString(ctx, "Promise was rejected with falsy value")); + JS_SetPropertyStr(ctx, args[0], "reason", JS_DupValue(ctx, value)); + } + args[1] = JS_UNDEFINED; + } else { + args[0] = JS_NULL; + args[1] = JS_DupValue(ctx, value); + } + JS_FreeValue(ctx, JS_Call(ctx, data[0], JS_UNDEFINED, 2, (JSValueConst *)args)); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + return JS_UNDEFINED; +} + +static JSValue sxn_callbackified(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *data) { + (void)magic; + if (argc < 1 || !JS_IsFunction(ctx, argv[argc - 1])) + return JS_ThrowTypeError(ctx, "the last argument must be a callback"); + JSValueConst callback = argv[argc - 1]; + JSValue result = JS_Call(ctx, data[0], this_val, argc - 1, argv); + if (JS_IsException(result)) return result; + + JSValue global = JS_GetGlobalObject(ctx); + JSValue promise_class = JS_GetPropertyStr(ctx, global, "Promise"); + JS_FreeValue(ctx, global); + JSValue resolve = JS_GetPropertyStr(ctx, promise_class, "resolve"); + JSValueConst resolve_args[1] = { result }; + JSValue promise = JS_Call(ctx, resolve, promise_class, 1, resolve_args); + JS_FreeValue(ctx, resolve); + JS_FreeValue(ctx, promise_class); + JS_FreeValue(ctx, result); + if (JS_IsException(promise)) return promise; + + JSValue handler_data[1] = { JS_DupValue(ctx, callback) }; + JSValue on_value = JS_NewCFunctionData(ctx, sxn_callbackify_settle, 1, 0, 1, handler_data); + JSValue on_error = JS_NewCFunctionData(ctx, sxn_callbackify_settle, 1, 1, 1, handler_data); + JS_FreeValue(ctx, handler_data[0]); + JSValue then = JS_GetPropertyStr(ctx, promise, "then"); + JSValueConst then_args[2] = { on_value, on_error }; + JS_FreeValue(ctx, JS_Call(ctx, then, promise, 2, then_args)); + JS_FreeValue(ctx, then); + JS_FreeValue(ctx, on_value); + JS_FreeValue(ctx, on_error); + JS_FreeValue(ctx, promise); + return JS_UNDEFINED; +} + +static JSValue js_callbackify(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1 || !JS_IsFunction(ctx, argv[0])) + return JS_ThrowTypeError(ctx, "callbackify expects a function"); + JSValue data[1] = { JS_DupValue(ctx, argv[0]) }; + JSValue wrapped = JS_NewCFunctionData(ctx, sxn_callbackified, 0, 0, 1, data); + JS_FreeValue(ctx, data[0]); + return wrapped; +} + +/* ---------------- assert's deep comparison, in C ---------------- + The whole of it is calls back into the engine -- reading properties, + comparing values, walking a Map -- so this is not faster than the + JavaScript it replaces. It is here because the compatibility layer is + being moved into C and this is the last piece of it that carries real + logic rather than glue. The measurement is in spec/NODE.md. + + Cycles are handled the way the specification's SameValue-based walk is: + a pair already being compared higher up the stack is taken as equal, + which terminates and matches what Node does for two identical cycles. */ +typedef struct SxnDeepPair { JSValueConst a, b; struct SxnDeepPair *prev; } SxnDeepPair; + +static int sxn_deep_equal(JSContext *ctx, JSValueConst a, JSValueConst b, bool strict, SxnDeepPair *seen, int depth); + +static bool sxn_same_value(JSContext *ctx, JSValueConst a, JSValueConst b) { + /* Object.is: NaN equals itself, +0 and -0 do not. */ + return JS_IsSameValue(ctx, a, b); +} + +static int sxn_deep_keys_equal(JSContext *ctx, JSValueConst a, JSValueConst b, bool strict, SxnDeepPair *seen, int depth) { + JSPropertyEnum *ka = NULL, *kb = NULL; + uint32_t na = 0, nb = 0; + int result = -1; + /* The strict comparison counts own enumerable symbol keys; the loose one + does not look at them at all. */ + int flags = JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY | (strict ? JS_GPN_SYMBOL_MASK : 0); + if (JS_GetOwnPropertyNames(ctx, &ka, &na, a, flags)) return -1; + if (JS_GetOwnPropertyNames(ctx, &kb, &nb, b, flags)) { + JS_FreePropertyEnum(ctx, ka, na); + return -1; + } + if (na != nb) { result = 0; goto done; } + for (uint32_t i = 0; i < na; i++) { + int has = JS_HasProperty(ctx, b, ka[i].atom); + if (has < 0) { result = -1; goto done; } + if (!has) { result = 0; goto done; } + JSValue va = JS_GetProperty(ctx, a, ka[i].atom); + if (JS_IsException(va)) { result = -1; goto done; } + JSValue vb = JS_GetProperty(ctx, b, ka[i].atom); + if (JS_IsException(vb)) { JS_FreeValue(ctx, va); result = -1; goto done; } + int same = sxn_deep_equal(ctx, va, vb, strict, seen, depth + 1); + JS_FreeValue(ctx, va); + JS_FreeValue(ctx, vb); + if (same <= 0) { result = same; goto done; } + } + result = 1; + done: + JS_FreePropertyEnum(ctx, ka, na); + JS_FreePropertyEnum(ctx, kb, nb); + return result; +} + +/* Every entry of a Map, matched against the other Map's entry for the same + key. Set membership is the same walk with the value ignored. */ +static int sxn_deep_map_equal(JSContext *ctx, JSValueConst a, JSValueConst b, bool is_map, bool strict, SxnDeepPair *seen, int depth) { + int result = -1; + JSValue iterator = JS_UNDEFINED, next = JS_UNDEFINED; + JSValue size_a = JS_GetPropertyStr(ctx, a, "size"); + JSValue size_b = JS_GetPropertyStr(ctx, b, "size"); + int32_t na = 0, nb = 0; + JS_ToInt32(ctx, &na, size_a); + JS_ToInt32(ctx, &nb, size_b); + JS_FreeValue(ctx, size_a); + JS_FreeValue(ctx, size_b); + if (na != nb) return 0; + + JSValue entries_fn = JS_GetPropertyStr(ctx, a, is_map ? "entries" : "values"); + iterator = JS_Call(ctx, entries_fn, a, 0, NULL); + JS_FreeValue(ctx, entries_fn); + if (JS_IsException(iterator)) goto done; + next = JS_GetPropertyStr(ctx, iterator, "next"); + if (JS_IsException(next)) goto done; + for (;;) { + JSValue step = JS_Call(ctx, next, iterator, 0, NULL); + if (JS_IsException(step)) goto done; + JSValue done_flag = JS_GetPropertyStr(ctx, step, "done"); + bool finished = JS_ToBool(ctx, done_flag); + JS_FreeValue(ctx, done_flag); + if (finished) { JS_FreeValue(ctx, step); break; } + JSValue entry = JS_GetPropertyStr(ctx, step, "value"); + JS_FreeValue(ctx, step); + JSValue key = is_map ? JS_GetPropertyUint32(ctx, entry, 0) : JS_DupValue(ctx, entry); + JSValue has_fn = JS_GetPropertyStr(ctx, b, "has"); + JSValueConst args[1] = { key }; + JSValue has = JS_Call(ctx, has_fn, b, 1, args); + JS_FreeValue(ctx, has_fn); + bool present = JS_ToBool(ctx, has); + JS_FreeValue(ctx, has); + if (!present) { + JS_FreeValue(ctx, key); JS_FreeValue(ctx, entry); + result = 0; goto done; + } + if (is_map) { + JSValue want = JS_GetPropertyUint32(ctx, entry, 1); + JSValue get_fn = JS_GetPropertyStr(ctx, b, "get"); + JSValueConst get_args[1] = { key }; + JSValue got = JS_Call(ctx, get_fn, b, 1, get_args); + JS_FreeValue(ctx, get_fn); + int same = sxn_deep_equal(ctx, want, got, strict, seen, depth + 1); + JS_FreeValue(ctx, want); + JS_FreeValue(ctx, got); + if (same <= 0) { + JS_FreeValue(ctx, key); JS_FreeValue(ctx, entry); + result = same; goto done; + } + } + JS_FreeValue(ctx, key); + JS_FreeValue(ctx, entry); + } + result = 1; + done: + JS_FreeValue(ctx, iterator); + JS_FreeValue(ctx, next); + return result; +} + +static int sxn_deep_equal(JSContext *ctx, JSValueConst a, JSValueConst b, bool strict, SxnDeepPair *seen, int depth) { + if (depth > 512) return JS_ThrowRangeError(ctx, "deep comparison too deep"), -1; + if (strict ? sxn_same_value(ctx, a, b) : JS_IsStrictEqual(ctx, a, b)) return 1; + if (!JS_IsObject(a) || !JS_IsObject(b)) { + /* An object never equals a primitive, either way round: the loose + comparison is loose about 1 and "1", not about boxes. */ + if (JS_IsObject(a) || JS_IsObject(b)) return 0; + if (strict) return sxn_same_value(ctx, a, b) ? 1 : 0; + /* NaN is its own match here, as it is in Node. */ + if (JS_IsSameValueZero(ctx, a, b)) return 1; + int eq = JS_IsEqual(ctx, a, b); + return eq < 0 ? -1 : (eq > 0 ? 1 : 0); + } + /* Two different functions are never equal, whatever they carry. */ + if (JS_IsFunction(ctx, a) || JS_IsFunction(ctx, b)) return 0; + + for (SxnDeepPair *p = seen; p; p = p->prev) + if (JS_IsStrictEqual(ctx, p->a, a) && JS_IsStrictEqual(ctx, p->b, b)) + return 1; /* already being compared: a cycle */ + SxnDeepPair pair = { a, b, seen }; + + if (strict) { + /* Only the strict comparison cares which class an object came from. */ + JSValue proto_a = JS_GetPrototype(ctx, a); + JSValue proto_b = JS_GetPrototype(ctx, b); + bool same_proto = JS_IsStrictEqual(ctx, proto_a, proto_b); + JS_FreeValue(ctx, proto_a); + JS_FreeValue(ctx, proto_b); + if (!same_proto) return 0; + } + + /* Dates, regexps, maps and sets compare by content rather than by their + properties. The class id is what instanceof would find and what a + subclass keeps, so it is read directly instead of by name. */ + static const char *probe_src = "[new Date(), /x/, new Map(), new Set()]"; + static JSClassID date_id, regexp_id, map_id, set_id; + if (!date_id) { + JSValue probe = JS_Eval(ctx, probe_src, strlen(probe_src), "", JS_EVAL_TYPE_GLOBAL); + JSValue v; + v = JS_GetPropertyUint32(ctx, probe, 0); date_id = JS_GetClassID(v); JS_FreeValue(ctx, v); + v = JS_GetPropertyUint32(ctx, probe, 1); regexp_id = JS_GetClassID(v); JS_FreeValue(ctx, v); + v = JS_GetPropertyUint32(ctx, probe, 2); map_id = JS_GetClassID(v); JS_FreeValue(ctx, v); + v = JS_GetPropertyUint32(ctx, probe, 3); set_id = JS_GetClassID(v); JS_FreeValue(ctx, v); + JS_FreeValue(ctx, probe); + } + JSClassID cls = JS_GetClassID(a); + /* Even the loose comparison keeps arrays, typed arrays and dates apart + from plain objects, which is what the class says. */ + if (cls != JS_GetClassID(b)) return 0; + int result = -2; + if (cls == date_id) { + JSValue fa = JS_GetPropertyStr(ctx, a, "getTime"); + JSValue va = JS_Call(ctx, fa, a, 0, NULL); + JSValue vb = JS_Call(ctx, fa, b, 0, NULL); + JS_FreeValue(ctx, fa); + double da = 0, db = 0; + JS_ToFloat64(ctx, &da, va); + JS_ToFloat64(ctx, &db, vb); + JS_FreeValue(ctx, va); + JS_FreeValue(ctx, vb); + result = (da == db || (isnan(da) && isnan(db))) ? 1 : 0; + } else if (cls == regexp_id) { + JSValue sa = JS_ToString(ctx, a), sb = JS_ToString(ctx, b); + result = JS_IsStrictEqual(ctx, sa, sb) ? 1 : 0; + JS_FreeValue(ctx, sa); + JS_FreeValue(ctx, sb); + } else if (cls == map_id) { + result = sxn_deep_map_equal(ctx, a, b, true, strict, &pair, depth); + } else if (cls == set_id) { + result = sxn_deep_map_equal(ctx, a, b, false, strict, &pair, depth); + } + if (result != -2) return result; + + /* A typed array compares as bytes. */ + size_t bytes_a = 0, bytes_b = 0; + uint8_t *raw_a = JS_GetUint8Array(ctx, &bytes_a, a); + if (!raw_a) JS_FreeValue(ctx, JS_GetException(ctx)); + uint8_t *raw_b = raw_a ? JS_GetUint8Array(ctx, &bytes_b, b) : NULL; + if (raw_a && !raw_b) JS_FreeValue(ctx, JS_GetException(ctx)); + if (raw_a && raw_b) + return (bytes_a == bytes_b && (bytes_a == 0 || memcmp(raw_a, raw_b, bytes_a) == 0)) ? 1 : 0; + + return sxn_deep_keys_equal(ctx, a, b, strict, &pair, depth); +} + +static JSValue js_deep_equal(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + (void)this_val; + if (argc < 2) return JS_NewBool(ctx, false); + int result = sxn_deep_equal(ctx, argv[0], argv[1], magic != 0, NULL, 0); + if (result < 0) return JS_EXCEPTION; + return JS_NewBool(ctx, result == 1); +} + +static const char *node_querystring_names[] = { "parse", "stringify", "escape", "unescape", "decode", "encode" }; +static const char *node_url_names[] = { + "URL", "URLSearchParams", "fileURLToPath", "pathToFileURL", "format", "parse", +}; +static const char *node_net_names[] = { + "isIP", "isIPv4", "isIPv6", "Socket", "Server", + "createConnection", "connect", "createServer", +}; +static const char *node_crypto_names[] = { + "createHash", "createHmac", "randomBytes", "randomUUID", "randomFillSync", + "getRandomValues", "randomInt", "timingSafeEqual", "getHashes", + "constants", "webcrypto", "subtle", "Hash", "Hmac", +}; +static const char *node_zlib_names[] = { + "constants", "gzip", "gunzip", "deflate", "inflate", "deflateRaw", + "inflateRaw", "unzip", + "gzipSync", "gunzipSync", "deflateSync", "inflateSync", "deflateRawSync", + "inflateRawSync", "unzipSync", + "createGzip", "createGunzip", "createDeflate", "createInflate", + "createDeflateRaw", "createInflateRaw", "createUnzip", +}; +static const char *node_tty_names[] = { "isatty", "ReadStream", "WriteStream" }; +static const char *node_string_decoder_names[] = { "StringDecoder" }; +static const char *node_timers_names[] = { + "setTimeout", "clearTimeout", "setInterval", "clearInterval", + "setImmediate", "clearImmediate", "promises", +}; +static const char *node_perf_hooks_names[] = { "performance", "PerformanceObserver" }; +static const char *node_module_names[] = { "createRequire", "builtinModules", "isBuiltin" }; +static const char *node_http_names[] = { + "createServer", "request", "get", "Server", "IncomingMessage", + "ServerResponse", "STATUS_CODES", "METHODS", +}; +static const char *node_stream_names[] = { + "Readable", "Writable", "Duplex", "Transform", "PassThrough", + "Stream", "pipeline", "finished", "promises", +}; +static const char *node_assert_names[] = { + "ok", "equal", "notEqual", "strictEqual", "notStrictEqual", "deepEqual", + "deepStrictEqual", "notDeepStrictEqual", "fail", "throws", "doesNotThrow", + "match", "strict", "AssertionError", +}; + +/* One shared init: pull the object off its global and re-export its keys. */ +typedef struct { const char *global; const char **names; size_t count; } NodeSimpleModule; + +static int node_simple_init(JSContext *ctx, JSModuleDef *m, + const char *global, const char **names, size_t count) { + JSValue obj = node_global_lookup(ctx, global); + JS_SetModuleExport(ctx, m, "default", JS_DupValue(ctx, obj)); + for (size_t i = 0; i < count; i++) + JS_SetModuleExport(ctx, m, names[i], JS_GetPropertyStr(ctx, obj, names[i])); + JS_FreeValue(ctx, obj); + return 0; +} + +#define NODE_SIMPLE_MODULE(tag, globalname, namearr) \ + static int node_##tag##_init(JSContext *ctx, JSModuleDef *m) { \ + return node_simple_init(ctx, m, globalname, namearr, countof(namearr)); \ + } \ + static JSModuleDef *sxn_init_module_node_##tag(JSContext *ctx, const char *name) { \ + JSModuleDef *m = JS_NewCModule(ctx, name, node_##tag##_init); \ + if (!m) return NULL; \ + JS_AddModuleExport(ctx, m, "default"); \ + for (size_t i = 0; i < countof(namearr); i++) \ + JS_AddModuleExport(ctx, m, namearr[i]); \ + return m; \ + } + +NODE_SIMPLE_MODULE(util, "__sxnUtil", node_util_names) +NODE_SIMPLE_MODULE(os, "__sxnOs", node_os_names) +NODE_SIMPLE_MODULE(querystring, "__sxnQuerystring", node_querystring_names) +NODE_SIMPLE_MODULE(url, "__sxnUrl", node_url_names) +NODE_SIMPLE_MODULE(assert, "__sxnAssert", node_assert_names) +NODE_SIMPLE_MODULE(stream, "__sxnStream", node_stream_names) +NODE_SIMPLE_MODULE(http, "__sxnHttp", node_http_names) +NODE_SIMPLE_MODULE(net, "__sxnNet", node_net_names) +NODE_SIMPLE_MODULE(crypto, "__sxnCrypto", node_crypto_names) +NODE_SIMPLE_MODULE(zlib, "__sxnZlib", node_zlib_names) +NODE_SIMPLE_MODULE(tty, "__sxnTty", node_tty_names) +NODE_SIMPLE_MODULE(string_decoder, "__sxnStringDecoder", node_string_decoder_names) +NODE_SIMPLE_MODULE(timers, "__sxnTimers", node_timers_names) +NODE_SIMPLE_MODULE(perf_hooks, "__sxnPerfHooks", node_perf_hooks_names) +NODE_SIMPLE_MODULE(module, "__sxnModule", node_module_names) +static const char *node_timers_promises_names[] = { "setTimeout", "setImmediate" }; +NODE_SIMPLE_MODULE(timers_promises, "__sxnTimersPromises", node_timers_promises_names) +static const char *node_stream_promises_names[] = { "pipeline", "finished" }; +NODE_SIMPLE_MODULE(stream_promises, "__sxnStreamPromises", node_stream_promises_names) + +static const char *node_child_process_names[] = { + "spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync", + "fork", "ChildProcess", +}; +static const char *node_dns_names[] = { + "lookup", "resolve", "resolve4", "resolve6", "resolveMx", "resolveTxt", + "resolveSrv", "resolveNs", "resolveCname", "reverse", "getServers", + "setServers", "promises", "Resolver", +}; +static const char *node_dns_promises_names[] = { "lookup", "resolve", "resolve4", "resolve6", "getServers" }; +static const char *node_https_names[] = { "request", "get", "Agent", "globalAgent", "createServer", "Server" }; +static const char *node_tls_names[] = { + "connect", "createServer", "TLSSocket", "Server", "createSecureContext", + "rootCertificates", "DEFAULT_MIN_VERSION", "DEFAULT_MAX_VERSION", +}; +static const char *node_http2_names[] = { + "constants", "connect", "createServer", "createSecureServer", "getDefaultSettings", +}; +static const char *node_stream_web_names[] = { + "ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamBYOBReader", + "ReadableStreamDefaultController", "ReadableByteStreamController", + "ReadableStreamBYOBRequest", "WritableStream", "WritableStreamDefaultWriter", + "WritableStreamDefaultController", "TransformStream", + "TransformStreamDefaultController", "ByteLengthQueuingStrategy", + "CountQueuingStrategy", "TextEncoderStream", "TextDecoderStream", + "CompressionStream", "DecompressionStream", +}; +static const char *node_vm_names[] = { + "runInThisContext", "runInNewContext", "runInContext", "createContext", + "isContext", "compileFunction", "Script", +}; +static const char *node_v8_names[] = { + "getHeapStatistics", "getHeapSpaceStatistics", "setFlagsFromString", + "serialize", "deserialize", "cachedDataVersionTag", +}; +static const char *node_worker_threads_names[] = { + "isMainThread", "threadId", "parentPort", "workerData", "resourceLimits", + "SHARE_ENV", "Worker", "MessageChannel", "MessagePort", "BroadcastChannel", + "markAsUntransferable", "moveMessagePortToContext", "receiveMessageOnPort", + "setEnvironmentData", "getEnvironmentData", +}; +static const char *node_cluster_names[] = { + "isPrimary", "isMaster", "isWorker", "worker", "workers", "settings", + "schedulingPolicy", "setupPrimary", "setupMaster", "fork", "disconnect", +}; +static const char *node_readline_names[] = { + "Interface", "createInterface", "clearLine", "clearScreenDown", "cursorTo", + "moveCursor", "emitKeypressEvents", "promises", +}; +static const char *node_readline_promises_names[] = { "Interface", "createInterface" }; +static const char *node_async_hooks_names[] = { + "AsyncLocalStorage", "AsyncResource", "executionAsyncId", "triggerAsyncId", + "executionAsyncResource", "createHook", +}; +static const char *node_inspector_names[] = { "url", "open", "close", "waitForDebugger", "console", "Session", "promises" }; + +NODE_SIMPLE_MODULE(child_process, "__sxnChildProcess", node_child_process_names) +NODE_SIMPLE_MODULE(dns, "__sxnDns", node_dns_names) +NODE_SIMPLE_MODULE(dns_promises, "__sxnDnsPromises", node_dns_promises_names) +NODE_SIMPLE_MODULE(https, "__sxnHttps", node_https_names) +NODE_SIMPLE_MODULE(tls, "__sxnTls", node_tls_names) +NODE_SIMPLE_MODULE(http2, "__sxnHttp2", node_http2_names) +NODE_SIMPLE_MODULE(stream_web, "__sxnStreamWeb", node_stream_web_names) +NODE_SIMPLE_MODULE(vm, "__sxnVm", node_vm_names) +NODE_SIMPLE_MODULE(v8, "__sxnV8", node_v8_names) +NODE_SIMPLE_MODULE(worker_threads, "__sxnWorkerThreads", node_worker_threads_names) +NODE_SIMPLE_MODULE(cluster, "__sxnCluster", node_cluster_names) +NODE_SIMPLE_MODULE(readline, "__sxnReadline", node_readline_names) +NODE_SIMPLE_MODULE(readline_promises, "__sxnReadlinePromises", node_readline_promises_names) +NODE_SIMPLE_MODULE(async_hooks, "__sxnAsyncHooks", node_async_hooks_names) +NODE_SIMPLE_MODULE(inspector, "__sxnInspector", node_inspector_names) + +static const char *node_dgram_names[] = { "Socket", "createSocket" }; +static const char *node_punycode_names[] = { "encode", "decode", "toASCII", "toUnicode", "ucs2", "version" }; +static const char *node_diagnostics_channel_names[] = { + "Channel", "channel", "hasSubscribers", "subscribe", "unsubscribe", "tracingChannel", +}; +NODE_SIMPLE_MODULE(dgram, "__sxnDgram", node_dgram_names) +NODE_SIMPLE_MODULE(punycode, "__sxnPunycode", node_punycode_names) +NODE_SIMPLE_MODULE(diagnostics_channel, "__sxnDiagnosticsChannel", node_diagnostics_channel_names) + +static const char *node_console_names[] = { "log", "error", "warn", "info", "debug", "trace", "Console" }; +NODE_SIMPLE_MODULE(console, "__sxnConsole", node_console_names) + +/* node:constants is a flat bag of numbers whose names differ by platform, so + it has no named exports to declare -- only the object itself. */ +static int node_constants_init(JSContext *ctx, JSModuleDef *m) { + JS_SetModuleExport(ctx, m, "default", node_global_lookup(ctx, "__sxnConstants")); + return 0; +} + +static JSModuleDef *sxn_init_module_node_constants(JSContext *ctx, const char *name) { + JSModuleDef *m = JS_NewCModule(ctx, name, node_constants_init); + if (!m) return NULL; + JS_AddModuleExport(ctx, m, "default"); + return m; +} + + + + +static const char *node_fs_export_names[] = { + "readFileSync", "writeFileSync", "existsSync", "statSync", "lstatSync", + "createReadStream", "Stats", "constants", +}; + +static int node_fs_init(JSContext *ctx, JSModuleDef *m) { + JSValue fs = node_global_lookup(ctx, "__sxnFs"); + JS_SetModuleExport(ctx, m, "default", JS_DupValue(ctx, fs)); + for (size_t i = 0; i < countof(node_fs_export_names); i++) + JS_SetModuleExport(ctx, m, node_fs_export_names[i], JS_GetPropertyStr(ctx, fs, node_fs_export_names[i])); + JS_FreeValue(ctx, fs); + return 0; +} + +static JSModuleDef *sxn_init_module_node_fs(JSContext *ctx, const char *name) { + JSModuleDef *m = JS_NewCModule(ctx, name, node_fs_init); + if (!m) return NULL; + JS_AddModuleExport(ctx, m, "default"); + for (size_t i = 0; i < countof(node_fs_export_names); i++) + JS_AddModuleExport(ctx, m, node_fs_export_names[i]); + return m; +} + +static const char *node_fs_promises_export_names[] = { "readFile", "writeFile", "stat", "lstat" }; + +static int node_fs_promises_init(JSContext *ctx, JSModuleDef *m) { + JSValue fsp = node_global_lookup(ctx, "__sxnFsPromises"); + JS_SetModuleExport(ctx, m, "default", JS_DupValue(ctx, fsp)); + for (size_t i = 0; i < countof(node_fs_promises_export_names); i++) + JS_SetModuleExport(ctx, m, node_fs_promises_export_names[i], JS_GetPropertyStr(ctx, fsp, node_fs_promises_export_names[i])); + JS_FreeValue(ctx, fsp); + return 0; +} + +static JSModuleDef *sxn_init_module_node_fs_promises(JSContext *ctx, const char *name) { + JSModuleDef *m = JS_NewCModule(ctx, name, node_fs_promises_init); + if (!m) return NULL; + JS_AddModuleExport(ctx, m, "default"); + for (size_t i = 0; i < countof(node_fs_promises_export_names); i++) + JS_AddModuleExport(ctx, m, node_fs_promises_export_names[i]); + return m; +} + +static int node_process_init(JSContext *ctx, JSModuleDef *m) { + JSValue process = node_global_lookup(ctx, "process"); + JS_SetModuleExport(ctx, m, "default", process); + return 0; +} + +static JSModuleDef *sxn_init_module_node_process(JSContext *ctx, const char *name) { + JSModuleDef *m = JS_NewCModule(ctx, name, node_process_init); + if (!m) return NULL; + JS_AddModuleExport(ctx, m, "default"); + return m; +} + +/* The memo below is a one-slot cache, so it cannot grow -- but it does hold + strong references to one arbitrary emitter's `_events` object, event name + and listener list until the next emit of a different shape replaces them. + That is a root a cycle sweep cannot see past, which is why the sweep drops + it first. Rebuilding costs one emit. */ +void sxn_free_ee_memo(JSContext *ctx) { + sxn_ee_memo_clear(ctx); +} + +void sxn_free_node_compat(JSContext *ctx) { + /* Release the call-site fusion caches here rather than leaving them to + JS_FreeContext, which returns early when anything still holds a context + reference and would leave the cached shapes outstanding. */ + JS_DisableEmitFusion(ctx); + JS_DisableBufferLengthFusion(ctx); + /* The cached atoms below hold real references for the life of the + context; without this they show up as leaks under --leak-check (only + visible in builds where the runtime's leak dumps are compiled in). + JS_FreeAtom is a no-op for the predefined ones. */ + sxn_ee_memo_clear(ctx); + JS_FreeAtom(ctx, sxn_atom_events); + JS_FreeAtom(ctx, sxn_atom_length); + JS_FreeAtom(ctx, sxn_atom_error); + JS_FreeAtom(ctx, sxn_atom_utf8); JS_FreeAtom(ctx, sxn_atom_utf8_dash); + JS_FreeAtom(ctx, sxn_atom_hex); JS_FreeAtom(ctx, sxn_atom_base64); + JS_FreeAtom(ctx, sxn_atom_base64url); JS_FreeAtom(ctx, sxn_atom_toBase64); + JS_FreeAtom(ctx, sxn_atom_latin1); JS_FreeAtom(ctx, sxn_atom_binary); + JS_FreeAtom(ctx, sxn_atom_ascii); JS_FreeAtom(ctx, sxn_atom_ucs2); + JS_FreeAtom(ctx, sxn_atom_ucs2_dash); JS_FreeAtom(ctx, sxn_atom_utf16le); + JS_FreeAtom(ctx, sxn_atom_utf16le_dash); + sxn_atom_latin1 = sxn_atom_binary = sxn_atom_ascii = JS_ATOM_NULL; + sxn_atom_ucs2 = sxn_atom_ucs2_dash = JS_ATOM_NULL; + sxn_atom_utf16le = sxn_atom_utf16le_dash = JS_ATOM_NULL; + sxn_atom_utf8 = sxn_atom_utf8_dash = sxn_atom_hex = JS_ATOM_NULL; + sxn_atom_base64 = sxn_atom_base64url = JS_ATOM_NULL; + sxn_atom_toBase64 = JS_ATOM_NULL; + sxn_atom_events = JS_ATOM_NULL; + sxn_atom_length = JS_ATOM_NULL; + sxn_atom_error = JS_ATOM_NULL; +} + +/* Node-API lives in src/napi.c and is installed from here, not from the + runtime's own surface: loading npm's compiled addons is Node emulation. + Sxn.ffi is the other half of the pair and sits on the runtime side. */ +void sxn_install_napi(JSContext *ctx, uv_loop_t *loop); + +/* Every node: module this runtime has, and the function that registers it. + Registration is not free -- a JSModuleDef plus an atom per export name -- + and a program that imports two of them used to pay for all forty-four at + startup. They are registered when the loader asks for one instead. */ +typedef struct { const char *name; JSModuleDef *(*init)(JSContext *, const char *); } SxnNodeModule; +static const SxnNodeModule sxn_node_modules[] = { + { "node:buffer", sxn_init_module_node_buffer }, + { "node:events", sxn_init_module_node_events }, + { "node:path", sxn_init_module_node_path }, + { "node:process", sxn_init_module_node_process }, + { "node:fs", sxn_init_module_node_fs }, + { "node:fs/promises", sxn_init_module_node_fs_promises }, + { "node:util", sxn_init_module_node_util }, + { "node:os", sxn_init_module_node_os }, + { "node:querystring", sxn_init_module_node_querystring }, + { "node:url", sxn_init_module_node_url }, + { "node:assert", sxn_init_module_node_assert }, + { "node:assert/strict", sxn_init_module_node_assert }, + { "node:stream", sxn_init_module_node_stream }, + { "node:http", sxn_init_module_node_http }, + { "node:net", sxn_init_module_node_net }, + { "node:crypto", sxn_init_module_node_crypto }, + { "node:zlib", sxn_init_module_node_zlib }, + { "node:tty", sxn_init_module_node_tty }, + { "node:string_decoder", sxn_init_module_node_string_decoder }, + { "node:timers", sxn_init_module_node_timers }, + { "node:timers/promises", sxn_init_module_node_timers_promises }, + { "node:stream/promises", sxn_init_module_node_stream_promises }, + { "node:perf_hooks", sxn_init_module_node_perf_hooks }, + { "node:module", sxn_init_module_node_module }, + { "node:child_process", sxn_init_module_node_child_process }, + { "node:dns", sxn_init_module_node_dns }, + { "node:dns/promises", sxn_init_module_node_dns_promises }, + { "node:https", sxn_init_module_node_https }, + { "node:tls", sxn_init_module_node_tls }, + { "node:http2", sxn_init_module_node_http2 }, + { "node:stream/web", sxn_init_module_node_stream_web }, + { "node:vm", sxn_init_module_node_vm }, + { "node:v8", sxn_init_module_node_v8 }, + { "node:worker_threads", sxn_init_module_node_worker_threads }, + { "node:cluster", sxn_init_module_node_cluster }, + { "node:readline", sxn_init_module_node_readline }, + { "node:readline/promises", sxn_init_module_node_readline_promises }, + { "node:async_hooks", sxn_init_module_node_async_hooks }, + { "node:inspector", sxn_init_module_node_inspector }, + { "node:dgram", sxn_init_module_node_dgram }, + { "node:punycode", sxn_init_module_node_punycode }, + { "node:diagnostics_channel", sxn_init_module_node_diagnostics_channel }, + { "node:console", sxn_init_module_node_console }, + { "node:constants", sxn_init_module_node_constants }, + { NULL, NULL }, +}; + +JSModuleDef *sxn_node_module_load(JSContext *ctx, const char *name) { + for (const SxnNodeModule *m = sxn_node_modules; m->name; m++) + if (!strcmp(m->name, name)) return m->init(ctx, name); + return NULL; +} int sxn_install_node_compat(JSContext *ctx, const char *exec_path) { if (sxn_atom_events == JS_ATOM_NULL) sxn_atom_events = JS_NewAtom(ctx, "_events"); @@ -1675,6 +4884,7 @@ int sxn_install_node_compat(JSContext *ctx, const char *exec_path) { #endif JS_SetPropertyStr(ctx, global, "__sxnExecPath", JS_NewString(ctx, exec_path ? exec_path : "sxn")); JS_SetPropertyStr(ctx, global, "__sxnCwd", JS_NewCFunction(ctx, js_sxn_cwd, "__sxnCwd", 0)); + JS_SetPropertyStr(ctx, global, "__sxnChdir", JS_NewCFunction(ctx, js_sxn_chdir, "__sxnChdir", 1)); /* Node names the OS and CPU; packages branch on them. Derived from the compiler's own target macros rather than a runtime uname call. */ JS_SetPropertyStr(ctx, global, "__sxnZlibDeflate", JS_NewCFunction(ctx, js_zlib_deflate, "__sxnZlibDeflate", 3)); @@ -1688,6 +4898,7 @@ int sxn_install_node_compat(JSContext *ctx, const char *exec_path) { JS_SetPropertyStr(ctx, global, "__sxnEeListenerCount", JS_NewCFunction(ctx, js_ee_listener_count, "listenerCount", 1)); JS_SetPropertyStr(ctx, global, "__sxnEeListeners", JS_NewCFunction(ctx, js_ee_listeners, "listeners", 1)); JS_SetPropertyStr(ctx, global, "__sxnEeRemoveAllListeners", JS_NewCFunction(ctx, js_ee_remove_all_listeners, "removeAllListeners", 1)); + JS_SetPropertyStr(ctx, global, "__sxnEeDefaultMax", JS_NewCFunction(ctx, js_ee_default_max, "defaultMaxListeners", 1)); JS_SetPropertyStr(ctx, global, "__sxnPosixJoin", JS_NewCFunction(ctx, js_path_posix_join, "join", 0)); JS_SetPropertyStr(ctx, global, "__sxnPosixResolve", JS_NewCFunction(ctx, js_path_posix_resolve, "resolve", 0)); JS_SetPropertyStr(ctx, global, "__sxnPosixNormalize", JS_NewCFunction(ctx, js_path_posix_normalize, "normalize", 1)); @@ -1696,6 +4907,106 @@ int sxn_install_node_compat(JSContext *ctx, const char *exec_path) { JS_SetPropertyStr(ctx, global, "__sxnPosixBasename", JS_NewCFunction(ctx, js_path_posix_basename, "basename", 2)); JS_SetPropertyStr(ctx, global, "__sxnPosixExtname", JS_NewCFunction(ctx, js_path_posix_extname, "extname", 1)); JS_SetPropertyStr(ctx, global, "__sxnPosixRelative", JS_NewCFunction(ctx, js_path_posix_relative, "relative", 2)); + JS_SetPropertyStr(ctx, global, "__sxnWinNormalize", JS_NewCFunction(ctx, js_path_win_normalize, "normalize", 1)); + JS_SetPropertyStr(ctx, global, "__sxnWinIsAbsolute", JS_NewCFunction(ctx, js_path_win_is_absolute, "isAbsolute", 1)); + JS_SetPropertyStr(ctx, global, "__sxnWinJoin", JS_NewCFunction(ctx, js_path_win_join, "join", 2)); + JS_SetPropertyStr(ctx, global, "__sxnWinResolve", JS_NewCFunction(ctx, js_path_win_resolve, "resolve", 2)); + JS_SetPropertyStr(ctx, global, "__sxnWinDirname", JS_NewCFunction(ctx, js_path_win_dirname, "dirname", 1)); + JS_SetPropertyStr(ctx, global, "__sxnWinBasename", JS_NewCFunction(ctx, js_path_win_basename, "basename", 2)); + JS_SetPropertyStr(ctx, global, "__sxnWinExtname", JS_NewCFunction(ctx, js_path_win_extname, "extname", 1)); + JS_SetPropertyStr(ctx, global, "__sxnWinRelative", JS_NewCFunction(ctx, js_path_win_relative, "relative", 2)); + { + /* name, magic */ + static const struct { const char *name; int magic; } reads[] = { + { "readUInt8", 1 }, { "readInt8", 1 | SXN_NUM_SIGNED }, + { "readUInt16LE", 2 }, { "readUInt16BE", 2 | SXN_NUM_BIG_END }, + { "readInt16LE", 2 | SXN_NUM_SIGNED }, { "readInt16BE", 2 | SXN_NUM_SIGNED | SXN_NUM_BIG_END }, + { "readUInt32LE", 4 }, { "readUInt32BE", 4 | SXN_NUM_BIG_END }, + { "readInt32LE", 4 | SXN_NUM_SIGNED }, { "readInt32BE", 4 | SXN_NUM_SIGNED | SXN_NUM_BIG_END }, + { "readFloatLE", 4 | SXN_NUM_FLOAT }, { "readFloatBE", 4 | SXN_NUM_FLOAT | SXN_NUM_BIG_END }, + { "readDoubleLE", 8 | SXN_NUM_FLOAT }, { "readDoubleBE", 8 | SXN_NUM_FLOAT | SXN_NUM_BIG_END }, + { "readBigUInt64LE", 8 | SXN_NUM_BIG_INT }, { "readBigUInt64BE", 8 | SXN_NUM_BIG_INT | SXN_NUM_BIG_END }, + { "readBigInt64LE", 8 | SXN_NUM_BIG_INT | SXN_NUM_SIGNED }, + { "readBigInt64BE", 8 | SXN_NUM_BIG_INT | SXN_NUM_SIGNED | SXN_NUM_BIG_END }, + }; + static const struct { const char *name; int magic; } writes[] = { + { "writeUInt8", 1 }, { "writeInt8", 1 | SXN_NUM_SIGNED }, + { "writeUInt16LE", 2 }, { "writeUInt16BE", 2 | SXN_NUM_BIG_END }, + { "writeInt16LE", 2 | SXN_NUM_SIGNED }, { "writeInt16BE", 2 | SXN_NUM_SIGNED | SXN_NUM_BIG_END }, + { "writeUInt32LE", 4 }, { "writeUInt32BE", 4 | SXN_NUM_BIG_END }, + { "writeInt32LE", 4 | SXN_NUM_SIGNED }, { "writeInt32BE", 4 | SXN_NUM_SIGNED | SXN_NUM_BIG_END }, + { "writeFloatLE", 4 | SXN_NUM_FLOAT }, { "writeFloatBE", 4 | SXN_NUM_FLOAT | SXN_NUM_BIG_END }, + { "writeDoubleLE", 8 | SXN_NUM_FLOAT }, { "writeDoubleBE", 8 | SXN_NUM_FLOAT | SXN_NUM_BIG_END }, + { "writeBigUInt64LE", 8 | SXN_NUM_BIG_INT }, { "writeBigUInt64BE", 8 | SXN_NUM_BIG_INT | SXN_NUM_BIG_END }, + { "writeBigInt64LE", 8 | SXN_NUM_BIG_INT | SXN_NUM_SIGNED }, + { "writeBigInt64BE", 8 | SXN_NUM_BIG_INT | SXN_NUM_SIGNED | SXN_NUM_BIG_END }, + }; + JSValue accessors = JS_NewObject(ctx); + for (size_t i = 0; i < countof(reads); i++) + JS_SetPropertyStr(ctx, accessors, reads[i].name, + JS_NewCFunctionMagic(ctx, js_buffer_read, reads[i].name, 1, JS_CFUNC_generic_magic, reads[i].magic)); + for (size_t i = 0; i < countof(writes); i++) + JS_SetPropertyStr(ctx, accessors, writes[i].name, + JS_NewCFunctionMagic(ctx, js_buffer_write_num, writes[i].name, 2, JS_CFUNC_generic_magic, writes[i].magic)); + JS_SetPropertyStr(ctx, accessors, "copy", JS_NewCFunction(ctx, js_buffer_copy, "copy", 4)); + JS_SetPropertyStr(ctx, accessors, "readUIntLE", JS_NewCFunctionMagic(ctx, js_buffer_read_var, "readUIntLE", 2, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, accessors, "readUIntBE", JS_NewCFunctionMagic(ctx, js_buffer_read_var, "readUIntBE", 2, JS_CFUNC_generic_magic, SXN_NUM_BIG_END)); + JS_SetPropertyStr(ctx, accessors, "readIntLE", JS_NewCFunctionMagic(ctx, js_buffer_read_var, "readIntLE", 2, JS_CFUNC_generic_magic, SXN_NUM_SIGNED)); + JS_SetPropertyStr(ctx, accessors, "readIntBE", JS_NewCFunctionMagic(ctx, js_buffer_read_var, "readIntBE", 2, JS_CFUNC_generic_magic, SXN_NUM_SIGNED | SXN_NUM_BIG_END)); + JS_SetPropertyStr(ctx, accessors, "writeUIntLE", JS_NewCFunctionMagic(ctx, js_buffer_write_var, "writeUIntLE", 3, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, accessors, "writeUIntBE", JS_NewCFunctionMagic(ctx, js_buffer_write_var, "writeUIntBE", 3, JS_CFUNC_generic_magic, SXN_NUM_BIG_END)); + JS_SetPropertyStr(ctx, accessors, "writeIntLE", JS_NewCFunctionMagic(ctx, js_buffer_write_var, "writeIntLE", 3, JS_CFUNC_generic_magic, SXN_NUM_SIGNED)); + JS_SetPropertyStr(ctx, accessors, "writeIntBE", JS_NewCFunctionMagic(ctx, js_buffer_write_var, "writeIntBE", 3, JS_CFUNC_generic_magic, SXN_NUM_SIGNED | SXN_NUM_BIG_END)); + JS_SetPropertyStr(ctx, accessors, "write", JS_NewCFunction(ctx, js_buffer_write, "write", 4)); + JS_SetPropertyStr(ctx, accessors, "swap16", JS_NewCFunctionMagic(ctx, js_buffer_swap, "swap16", 0, JS_CFUNC_generic_magic, 2)); + JS_SetPropertyStr(ctx, accessors, "swap32", JS_NewCFunctionMagic(ctx, js_buffer_swap, "swap32", 0, JS_CFUNC_generic_magic, 4)); + JS_SetPropertyStr(ctx, accessors, "swap64", JS_NewCFunctionMagic(ctx, js_buffer_swap, "swap64", 0, JS_CFUNC_generic_magic, 8)); + JS_SetPropertyStr(ctx, global, "__sxnBufferAccessors", accessors); + } + JS_SetPropertyStr(ctx, global, "__sxnCallbackify", JS_NewCFunction(ctx, js_callbackify, "callbackify", 1)); + JS_SetPropertyStr(ctx, global, "__sxnInherits", JS_NewCFunction(ctx, js_inherits, "inherits", 2)); + JS_SetPropertyStr(ctx, global, "__sxnBufferFromBytes", JS_NewCFunction(ctx, js_buffer_from_bytes, "__sxnBufferFromBytes", 1)); + JS_SetPropertyStr(ctx, global, "__sxnPipe", JS_NewCFunction(ctx, js_stream_pipe, "pipe", 2)); + JS_SetPropertyStr(ctx, global, "__sxnPromisify", JS_NewCFunction(ctx, js_promisify, "promisify", 1)); + JS_SetPropertyStr(ctx, global, "__sxnInspect", JS_NewCFunction(ctx, js_inspect, "inspect", 2)); + JS_SetPropertyStr(ctx, global, "__sxnNextTick", JS_NewCFunction(ctx, js_next_tick, "nextTick", 1)); + JS_SetPropertyStr(ctx, global, "__sxnGetHeaders", JS_NewCFunctionMagic(ctx, js_header_list, "getHeaders", 0, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, global, "__sxnGetHeaderNames", JS_NewCFunctionMagic(ctx, js_header_list, "getHeaderNames", 0, JS_CFUNC_generic_magic, 1)); + JS_SetPropertyStr(ctx, global, "__sxnDecodeChunk", JS_NewCFunction(ctx, js_decode_chunk, "__sxnDecodeChunk", 2)); + JS_SetPropertyStr(ctx, global, "__sxnPathToFileUrl", JS_NewCFunction(ctx, js_path_to_file_url, "pathToFileURL", 1)); + JS_SetPropertyStr(ctx, global, "__sxnFileUrlToPath", JS_NewCFunction(ctx, js_file_url_to_path, "fileURLToPath", 1)); + JS_SetPropertyStr(ctx, global, "__sxnWriteCallback", JS_NewCFunction(ctx, js_write_callback, "__sxnWriteCallback", 2)); + JS_SetPropertyStr(ctx, global, "__sxnConcatBytes", JS_NewCFunction(ctx, js_concat_bytes, "__sxnConcatBytes", 2)); + JS_SetPropertyStr(ctx, global, "__sxnSetHeader", JS_NewCFunctionMagic(ctx, js_header_op, "setHeader", 2, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, global, "__sxnGetHeader", JS_NewCFunctionMagic(ctx, js_header_op, "getHeader", 1, JS_CFUNC_generic_magic, 1)); + JS_SetPropertyStr(ctx, global, "__sxnHasHeader", JS_NewCFunctionMagic(ctx, js_header_op, "hasHeader", 1, JS_CFUNC_generic_magic, 2)); + JS_SetPropertyStr(ctx, global, "__sxnRemoveHeader", JS_NewCFunctionMagic(ctx, js_header_op, "removeHeader", 1, JS_CFUNC_generic_magic, 3)); + JS_SetPropertyStr(ctx, global, "__sxnHttpReadBody", JS_NewCFunction(ctx, js_http_read_body, "_read", 0)); + JS_SetPropertyStr(ctx, global, "__sxnHttpComplete", JS_NewCFunction(ctx, js_http_complete, "onEnd", 0)); + JS_SetPropertyStr(ctx, global, "__sxnHttpSocket", JS_NewCFunction(ctx, js_http_socket, "__sxnHttpSocket", 0)); + JS_SetPropertyStr(ctx, global, "__sxnEeOnce", JS_NewCFunction(ctx, js_ee_once, "once", 2)); + JS_SetPropertyStr(ctx, global, "__sxnJoinChunks", JS_NewCFunction(ctx, js_join_chunks, "__sxnJoinChunks", 1)); + JS_SetPropertyStr(ctx, global, "__sxnHttpHeaders", JS_NewCFunction(ctx, js_http_headers, "__sxnHttpHeaders", 1)); + JS_SetPropertyStr(ctx, global, "__sxnBuiltinRequire", JS_NewCFunction(ctx, js_builtin_require, "__sxnBuiltinRequire", 1)); + JS_SetPropertyStr(ctx, global, "__sxnZlibStreamNew", JS_NewCFunction(ctx, js_zlib_stream_new, "__sxnZlibStreamNew", 3)); + JS_SetPropertyStr(ctx, global, "__sxnZlibStreamPush", JS_NewCFunction(ctx, js_zlib_stream_push, "__sxnZlibStreamPush", 3)); + JS_SetPropertyStr(ctx, global, "__sxnBuiltinNames", JS_NewCFunction(ctx, js_builtin_names, "__sxnBuiltinNames", 0)); + JS_SetPropertyStr(ctx, global, "__sxnIsBuiltin", JS_NewCFunction(ctx, js_is_builtin, "__sxnIsBuiltin", 1)); + JS_SetPropertyStr(ctx, global, "__sxnLatin1Bytes", JS_NewCFunctionMagic(ctx, js_buffer_encode_units, "__sxnLatin1Bytes", 1, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, global, "__sxnUtf16leBytes", JS_NewCFunctionMagic(ctx, js_buffer_encode_units, "__sxnUtf16leBytes", 1, JS_CFUNC_generic_magic, 1)); + JS_SetPropertyStr(ctx, global, "__sxnLatin1String", JS_NewCFunctionMagic(ctx, js_buffer_decode_units, "__sxnLatin1String", 1, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, global, "__sxnAsciiString", JS_NewCFunctionMagic(ctx, js_buffer_decode_units, "__sxnAsciiString", 1, JS_CFUNC_generic_magic, 1)); + JS_SetPropertyStr(ctx, global, "__sxnUtf16leString", JS_NewCFunctionMagic(ctx, js_buffer_decode_units, "__sxnUtf16leString", 1, JS_CFUNC_generic_magic, 2)); + JS_SetPropertyStr(ctx, global, "__sxnDeepEqual", JS_NewCFunctionMagic(ctx, js_deep_equal, "__sxnDeepEqual", 2, JS_CFUNC_generic_magic, 1)); + JS_SetPropertyStr(ctx, global, "__sxnLooseDeepEqual", JS_NewCFunctionMagic(ctx, js_deep_equal, "__sxnLooseDeepEqual", 2, JS_CFUNC_generic_magic, 0)); + JS_SetPropertyStr(ctx, global, "__sxnFormat", JS_NewCFunction(ctx, js_util_format, "__sxnFormat", 3)); + JS_SetPropertyStr(ctx, global, "__sxnHexBytes", JS_NewCFunction(ctx, js_hex_bytes, "__sxnHexBytes", 1)); + JS_SetPropertyStr(ctx, global, "__sxnBase64Bytes", JS_NewCFunction(ctx, js_base64_bytes, "__sxnBase64Bytes", 1)); + JS_SetPropertyStr(ctx, global, "__sxnIsIP", JS_NewCFunction(ctx, js_net_is_ip, "isIP", 1)); + JS_SetPropertyStr(ctx, global, "__sxnQsParse", JS_NewCFunction(ctx, js_qs_parse, "parse", 4)); + JS_SetPropertyStr(ctx, global, "__sxnQsStringify", JS_NewCFunction(ctx, js_qs_stringify, "stringify", 3)); + JS_SetPropertyStr(ctx, global, "__sxnQsEscape", JS_NewCFunction(ctx, js_qs_escape, "escape", 1)); + JS_SetPropertyStr(ctx, global, "__sxnQsUnescape", JS_NewCFunction(ctx, js_qs_unescape, "unescape", 1)); JS_SetPropertyStr(ctx, global, "__sxnExit", JS_NewCFunction(ctx, js_sxn_exit, "__sxnExit", 1)); JS_SetPropertyStr(ctx, global, "__sxnWatchSignal", JS_NewCFunction(ctx, js_sxn_watch_signal, "__sxnWatchSignal", 2)); JS_SetPropertyStr(ctx, global, "__sxnReadFileSync", JS_NewCFunction(ctx, js_sxn_read_file_sync, "__sxnReadFileSync", 1)); @@ -1722,31 +5033,5 @@ int sxn_install_node_compat(JSContext *ctx, const char *exec_path) { JS_FreeValue(ctx, result); sxn_install_buffer_natives(ctx); - if (!sxn_init_module_node_buffer(ctx, "node:buffer")) return -1; - if (!sxn_init_module_node_events(ctx, "node:events")) return -1; - if (!sxn_init_module_node_path(ctx, "node:path")) return -1; - if (!sxn_init_module_node_process(ctx, "node:process")) return -1; - if (!sxn_init_module_node_fs(ctx, "node:fs")) return -1; - if (!sxn_init_module_node_fs_promises(ctx, "node:fs/promises")) return -1; - if (!sxn_init_module_node_util(ctx, "node:util")) return -1; - if (!sxn_init_module_node_os(ctx, "node:os")) return -1; - if (!sxn_init_module_node_querystring(ctx, "node:querystring")) return -1; - if (!sxn_init_module_node_url(ctx, "node:url")) return -1; - if (!sxn_init_module_node_assert(ctx, "node:assert")) return -1; - if (!sxn_init_module_node_assert(ctx, "node:assert/strict")) return -1; - if (!sxn_init_module_node_stream(ctx, "node:stream")) return -1; - if (!sxn_init_module_node_http(ctx, "node:http")) return -1; - if (!sxn_init_module_node_net(ctx, "node:net")) return -1; - if (!sxn_init_module_node_crypto(ctx, "node:crypto")) return -1; - if (!sxn_init_module_node_zlib(ctx, "node:zlib")) return -1; - if (!sxn_init_module_node_tty(ctx, "node:tty")) return -1; - if (!sxn_init_module_node_string_decoder(ctx, "node:string_decoder")) return -1; - if (!sxn_init_module_node_timers(ctx, "node:timers")) return -1; - /* The promises sub-path is a distinct specifier; register the object it - names directly rather than re-exporting the parent. */ - if (!sxn_init_module_node_timers_promises(ctx, "node:timers/promises")) return -1; - if (!sxn_init_module_node_stream_promises(ctx, "node:stream/promises")) return -1; - if (!sxn_init_module_node_perf_hooks(ctx, "node:perf_hooks")) return -1; - if (!sxn_init_module_node_module(ctx, "node:module")) return -1; return 0; } diff --git a/src/node_compat.js b/src/node_compat.js index 2b8621d..4e7b4ea 100644 --- a/src/node_compat.js +++ b/src/node_compat.js @@ -1,5 +1,5 @@ /* Minimal `node:*` compatibility layer, layered the same way bootstrap.js - layers WinterCG globals: pure spec/behavior logic lives here in JS, native + layers WinterTC globals: pure spec/behavior logic lives here in JS, native primitives (env, cwd, exit, signal watching) are thin C wrappers installed by sxn_install_node_compat before this file is evaluated. The four `node:*` C modules (see src/node.c) just re-export the globals this file @@ -11,23 +11,16 @@ // replacing node_compat.js with native C. Listener storage is still the // plain `this._events` object with Node's function-for-one-listener and // array-for-many representation; the native side reads and writes it - // directly. `once` stays JS: its self-removing wrapper has no natural - // native shape (it needs to reference itself to call `self.off(type, - // wrapped)`), and it's not a hot path the way emit() is. + // directly. `once` went native too (js_ee_once): the self-removing wrapper + // does have a native shape after all -- a C function carrying the emitter, + // the name and the listener, and one holder object it reads itself back + // out of. 0.55 microseconds per once-and-emit became 0.44. function EventEmitter() { this._events = Object.create(null); } EventEmitter.prototype.on = __sxnEeOn; EventEmitter.prototype.addListener = __sxnEeOn; - EventEmitter.prototype.once = function (type, listener) { - var self = this; - function wrapped() { - self.off(type, wrapped); - listener.apply(this, arguments); - } - wrapped._original = listener; - return this.on(type, wrapped); - }; + EventEmitter.prototype.once = __sxnEeOnce; EventEmitter.prototype.off = __sxnEeOff; EventEmitter.prototype.removeListener = __sxnEeOff; EventEmitter.prototype.removeAllListeners = __sxnEeRemoveAllListeners; @@ -76,98 +69,78 @@ // No `.default` here: Node does not define one, and the ESM default export // is set on the module itself rather than as a property of the class. EventEmitter.captureRejectionSymbol = Symbol.for("nodejs.rejection"); - EventEmitter.defaultMaxListeners = 10; + // A listener count crossing this limit is Node's standard signal that + // handlers are being added and never removed, which is how a long-running + // server grows one closure at a time. The count check lives with the rest + // of the native listener code (sxn_ee_check_max_listeners in src/node.c), + // so the default lives there too and this is the accessor over it -- + // `require('events').defaultMaxListeners = 20` still reads and writes the + // value the check actually uses. 0 or less means unlimited, as in Node. + Object.defineProperty(EventEmitter, "defaultMaxListeners", { + get: () => __sxnEeDefaultMax(), + set: (n) => { __sxnEeDefaultMax(n); }, + enumerable: true, configurable: true, + }); + // Per-emitter override. Without these the warning would be unsuppressable, + // which is worse than not warning: a program that legitimately wants 50 + // listeners could do nothing about the noise. + EventEmitter.prototype.setMaxListeners = function (n) { + this._maxListeners = n; + return this; + }; + EventEmitter.prototype.getMaxListeners = function () { + return typeof this._maxListeners === "number" + ? this._maxListeners + : EventEmitter.defaultMaxListeners; + }; globalThis.__sxnEventEmitter = EventEmitter; delete globalThis.__sxnEeOn; + delete globalThis.__sxnEeOnce; delete globalThis.__sxnEeOff; delete globalThis.__sxnEeEmit; delete globalThis.__sxnEeListenerCount; delete globalThis.__sxnEeListeners; delete globalThis.__sxnEeRemoveAllListeners; + // __sxnEeDefaultMax stays on the global: the accessor above closes over it. // ---------------- buffer: Buffer ---------------- // Only called for non-utf-8 encodings -- Buffer.from's string branch // handles utf-8 itself via a native primitive that skips the extra // allocation this returns-a-bare-Uint8Array shape would otherwise cost. - // `encoding` arrives already-lowercased from that call site. - // Node decodes as much as it can and stops, where the standard - // Uint8Array.fromHex throws. Take the strict, native path first and fall - // back only when it refuses, so valid input keeps its speed. - function hexBytesLenient(str) { - var out = new Uint8Array(str.length >> 1), n = 0; - for (var i = 0; i + 1 < str.length; i += 2) { - var hi = hexDigit(str.charCodeAt(i) & 0xff), lo = hexDigit(str.charCodeAt(i + 1) & 0xff); - if (hi < 0 || lo < 0) break; - out[n++] = (hi << 4) | lo; - } - return out.subarray(0, n); - } - function hexDigit(c) { - if (c >= 48 && c <= 57) return c - 48; - if (c >= 97 && c <= 102) return c - 87; - if (c >= 65 && c <= 70) return c - 55; - return -1; - } - - // Node's base64 reader takes either alphabet, skips anything that is not a - // base64 character, and does not require padding. - var B64 = (function () { - var t = new Int8Array(128).fill(-1); - var a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (var i = 0; i < a.length; i++) t[a.charCodeAt(i)] = i; - t[43] = 62; t[47] = 63; // + / - t[45] = 62; t[95] = 63; // - _ (base64url) - return t; - })(); - function base64BytesLenient(str) { - var out = new Uint8Array((str.length * 3) >> 2), n = 0, acc = 0, bits = 0; - for (var i = 0; i < str.length; i++) { - // Node reads the string one byte at a time, so a code unit above 0xff is - // truncated rather than skipped -- which is why an emoji ends a base64 - // string: the high half of its surrogate pair masks down to '='. - var c = str.charCodeAt(i) & 0xff; - if (c === 61) break; // '=' ends the data - var v = c < 128 ? B64[c] : -1; - if (v < 0) continue; - acc = (acc << 6) | v; bits += 6; - if (bits >= 8) { bits -= 8; out[n++] = (acc >> bits) & 0xff; } - } - return out.subarray(0, n); - } - - function utf16leBytes(str) { - var out = new Uint8Array(str.length * 2); - for (var i = 0; i < str.length; i++) { - var c = str.charCodeAt(i); - out[i * 2] = c & 0xff; - out[i * 2 + 1] = c >> 8; - } - return out; - } - function utf16leString(bytes) { - var out = ""; - for (var i = 0; i + 1 < bytes.length; i += 2) - out += String.fromCharCode(bytes[i] | (bytes[i + 1] << 8)); - return out; - } + // `encoding` arrives already-lowercased from that call site. The lenient + // hex and base64 readers Node's leniency needs are native (js_hex_bytes + // and js_base64_bytes), including the case where a string holds code + // units above 0xff and Node's byte-at-a-time reading of it shows. + var utf16leBytes = __sxnUtf16leBytes; + // Native (js_buffer_decode_units in src/node.c): latin1, Node's 7-bit + // "ascii" and utf16le are all a widening of bytes into code units, which + // was a String.fromCharCode per byte here. + var utf16leString = __sxnUtf16leString; function bufferBytesFromString(str, encoding) { + // The native readers are lenient the way Node is -- hex stops at the + // first pair that is not hex, base64 skips anything outside the alphabet + // -- so ordinary payloads, including base64 with the newlines PEM and + // MIME put in it, never touch the JavaScript loops. They hand back null + // for a string with anything non-ASCII in it, where Node's reading of + // UTF-16 code units is visible, and the loops below take that. if (encoding === "hex") { - try { return Uint8Array.fromHex(str); } catch { return hexBytesLenient(str); } + // Strict first, because it reads the string's own bytes with no copy + // at all; the native lenient reader takes over when the input has + // something in it that the strict one refuses. + try { return Uint8Array.fromHex(str); } catch { /* fall through */ } + return __sxnHexBytes(str); } if (encoding === "base64" || encoding === "base64url") { try { return encoding === "base64" ? Uint8Array.fromBase64(str) : Uint8Array.fromBase64(str, { alphabet: "base64url" }); - } catch { return base64BytesLenient(str); } + } catch { /* fall through */ } + return __sxnBase64Bytes(str); } if (encoding === "ucs2" || encoding === "ucs-2" || encoding === "utf16le" || encoding === "utf-16le") return utf16leBytes(str); - if (encoding === "latin1" || encoding === "binary" || encoding === "ascii") { - var bytes = new Uint8Array(str.length); - for (var i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i) & 0xff; - return bytes; - } + if (encoding === "latin1" || encoding === "binary" || encoding === "ascii") return __sxnLatin1Bytes(str); throw new TypeError("Unknown encoding: " + encoding); } @@ -189,17 +162,9 @@ if (encoding === "base64url") return this.toBase64({ alphabet: "base64url", omitPadding: true }); // Node emits base64url unpadded if (encoding === "ucs2" || encoding === "ucs-2" || encoding === "utf16le" || encoding === "utf-16le") return utf16leString(this); - if (encoding === "latin1" || encoding === "binary") { - var out = ""; - for (var i = 0; i < this.length; i++) out += String.fromCharCode(this[i]); - return out; - } - if (encoding === "ascii") { - // Node's "ascii" is 7-bit: the high bit is stripped, unlike latin1. - var a = ""; - for (var j = 0; j < this.length; j++) a += String.fromCharCode(this[j] & 0x7f); - return a; - } + if (encoding === "latin1" || encoding === "binary") return __sxnLatin1String(this); + // Node's "ascii" is 7-bit: the high bit is stripped, unlike latin1. + if (encoding === "ascii") return __sxnAsciiString(this); throw new TypeError("Unknown encoding: " + encoding); } // Node's Buffer#slice (and #subarray) are zero-copy views over the same @@ -238,9 +203,16 @@ if (enc === "utf-8" || enc === "utf8") return new Buffer(__sxnUtf8EncodeArrayBuffer(data)); return Object.setPrototypeOf(bufferBytesFromString(data, enc), Buffer.prototype); } - if (data instanceof ArrayBuffer) return new Buffer(data); // zero-copy view over the whole buffer - if (ArrayBuffer.isView(data)) return new Buffer(data.buffer, data.byteOffset, data.byteLength); // zero-copy view - if (Array.isArray(data) || (data && typeof data.length === "number")) return new Buffer(data); // copies, matching Node + if (data instanceof ArrayBuffer) return new Buffer(data); // a view, which is what Node gives for an ArrayBuffer + // Native (js_buffer_from_bytes in src/node.c): copying a view went + // through the Uint8Array subclass constructor, 0.285us against + // 0.210us for the copy made in C. Node copies here, and code relies + // on it -- writing to the copy must not reach the original. + if (ArrayBuffer.isView(data)) return __sxnBufferFromBytes(data); + // An array stays with the constructor: reading its elements one at a + // time from C measured 1.185us against the engine's own 0.375us for + // the same array, which it fills without leaving the interpreter. + if (Array.isArray(data) || (data && typeof data.length === "number")) return new Buffer(data); throw new TypeError("Buffer.from: unsupported argument"); } // Node serializes a Buffer as { type: "Buffer", data: [...] }, and code @@ -250,161 +222,33 @@ // Node orders by unsigned byte value, then by length, and equals is just // compare === 0. Anything holding bytes is accepted, as Node does. compare(other) { - var n = Math.min(this.length, other.length); - for (var i = 0; i < n; i++) { - if (this[i] !== other[i]) return this[i] < other[i] ? -1 : 1; - } - return this.length === other.length ? 0 : (this.length < other.length ? -1 : 1); + // Native (sxn_bytes_compare in src/network.c): memcmp, then length. + return __sxnBytesCompare(this, other instanceof Uint8Array ? other : Buffer.from(other)); } equals(other) { return this.length === other.length && this.compare(other) === 0; } + // Native (js_concat_bytes in src/node.c): one memcpy per part rather + // than a length loop, a subarray per part and a set() per part. static concat(list, totalLength) { - if (totalLength === undefined) { - totalLength = 0; - for (var i = 0; i < list.length; i++) totalLength += list[i].length; - } - var out = new Buffer(totalLength); - var offset = 0; - for (var j = 0; j < list.length && offset < totalLength; j++) { - var chunk = list[j].subarray(0, Math.min(list[j].length, totalLength - offset)); - out.set(chunk, offset); - offset += chunk.length; - } - return out; + return Object.setPrototypeOf(__sxnConcatBytes(list, totalLength), Buffer.prototype); } } + // The numeric accessors -- readUInt32BE, writeFloatLE and the rest -- and + // copy, all native (js_buffer_read/js_buffer_write_num/js_buffer_copy in + // src/node.c). They are pure byte-to-number work, they are what binary + // protocol code spends its time in, and none of them existed here before. + Object.assign(Buffer.prototype, __sxnBufferAccessors); + delete globalThis.__sxnBufferAccessors; + + Buffer.compare = (a, b) => __sxnBytesCompare(a, b); + Buffer.isEncoding = (enc) => + ["utf8", "utf-8", "hex", "base64", "base64url", "latin1", "binary", "ascii", "ucs2", "ucs-2", "utf16le", "utf-16le"] + .includes(String(enc).toLowerCase()); + Buffer.poolSize = 8192; + globalThis.Buffer = Buffer; // ---------------- path: posix / win32 ---------------- - // Reduces a split path into a normalized segment list: drops "." and - // empty segments, resolves ".." against the previous real segment, and - // (for relative paths) keeps a leading run of ".." since there's no root - // to clamp against. - function reduceSegments(parts, isAbsolutePath) { - var out = []; - for (var i = 0; i < parts.length; i++) { - var seg = parts[i]; - if (seg === "" || seg === ".") continue; - if (seg === "..") { - if (out.length && out[out.length - 1] !== "..") out.pop(); - else if (!isAbsolutePath) out.push(".."); - } else { - out.push(seg); - } - } - return out; - } - - function makePathImpl(sep, delimiter, splitRe, isAbsoluteFn, formatRoot) { - function normalize(p) { - p = String(p); - if (p === "") return "."; - var isAbs = isAbsoluteFn(p); - var root = formatRoot(p, isAbs); - var rest = p.slice(root.rootLength); - var trailingSep = rest.length > 0 && splitRe.test(rest.charAt(rest.length - 1)); - var segments = reduceSegments(rest.split(splitRe), isAbs); - var out = root.prefix + segments.join(sep); - if (out === "") out = "."; - if (trailingSep && segments.length && out.charAt(out.length - 1) !== sep) out += sep; - return out; - } - - function join() { - var parts = []; - for (var i = 0; i < arguments.length; i++) { - var a = arguments[i]; - if (a === undefined || a === null) continue; - if (typeof a !== "string") throw new TypeError("path segments must be strings"); - if (a.length) parts.push(a); - } - if (!parts.length) return "."; - return normalize(parts.join(sep)); - } - - function resolve() { - var resolved = ""; - var resolvedAbsolute = false; - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var seg = i >= 0 ? arguments[i] : __sxnCwd(); - if (!seg) continue; - resolved = seg + sep + resolved; - resolvedAbsolute = isAbsoluteFn(seg); - } - var out = normalize(resolved); - if (!resolvedAbsolute) out = normalize(__sxnCwd() + sep + resolved); - // strip a normalize()-added trailing separator, resolve() never keeps one - var root = formatRoot(out, true); - if (out.length > root.rootLength && splitRe.test(out.charAt(out.length - 1))) out = out.slice(0, -1); - return out; - } - - function dirname(p) { - p = String(p); - var isAbs = isAbsoluteFn(p); - var root = formatRoot(p, isAbs); - var rest = p.slice(root.rootLength); - var end = -1, matchedSep = true; - for (var i = rest.length - 1; i >= 0; i--) { - if (splitRe.test(rest.charAt(i))) { - if (!matchedSep) { end = i; break; } - } else matchedSep = false; - } - if (end === -1) return root.rootLength ? (root.prefix || root.rootPath) : "."; - return root.prefix + rest.slice(0, end); - } - - function basename(p, suffix) { - p = String(p); - var root = formatRoot(p, isAbsoluteFn(p)); - var rest = p.slice(root.rootLength).replace(new RegExp("[" + (sep === "\\" ? "\\\\/" : "/") + "]+$"), ""); - var idx = -1; - for (var i = rest.length - 1; i >= 0; i--) { if (splitRe.test(rest.charAt(i))) { idx = i; break; } } - var base = idx === -1 ? rest : rest.slice(idx + 1); - if (suffix && base.length > suffix.length && base.slice(-suffix.length) === suffix) { - base = base.slice(0, -suffix.length); - } - return base; - } - - function extname(p) { - var base = basename(p); - var dot = base.lastIndexOf("."); - if (dot <= 0) return ""; // no dot, or a leading dot (e.g. ".gitignore") - return base.slice(dot); - } - - function relative(from, to) { - from = resolve(from); - to = resolve(to); - if (from === to) return ""; - var fromParts = from.split(sep).filter(Boolean); - var toParts = to.split(sep).filter(Boolean); - var common = 0; - while (common < fromParts.length && common < toParts.length && - (sep === "\\" ? fromParts[common].toLowerCase() === toParts[common].toLowerCase() : fromParts[common] === toParts[common])) { - common++; - } - var ups = fromParts.length - common; - var out = []; - for (var i = 0; i < ups; i++) out.push(".."); - return out.concat(toParts.slice(common)).join(sep); - } - - return { - sep: sep, - delimiter: delimiter, - isAbsolute: function (p) { return isAbsoluteFn(String(p)); }, - normalize: normalize, - join: join, - resolve: resolve, - dirname: dirname, - basename: basename, - extname: extname, - relative: relative, - }; - } - // Native (see js_path_posix_* in src/node.c, registered as globals above) // -- phase 4 of replacing node_compat.js with native C. Same // reduceSegments/makePathImpl algorithm, ported to walk C strings @@ -423,27 +267,21 @@ relative: __sxnPosixRelative, }; - var WIN32_SPLIT_RE = /[\\/]/; - function win32Root(p) { - // UNC: \\server\share\... - var unc = /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/.exec(p); - if (unc) return { rootLength: unc[0].length, prefix: unc[0].replace(/[\\/]+$/, "\\") + "\\", rootPath: unc[0] }; - // Drive-qualified: C:\... or C:... - var drive = /^[a-zA-Z]:[\\/]?/.exec(p); - if (drive) { - var withSep = /[\\/]$/.test(drive[0]); - return { rootLength: drive[0].length, prefix: drive[0].slice(0, 2) + (withSep ? "\\" : ""), rootPath: drive[0].slice(0, 2) + "\\" }; - } - if (WIN32_SPLIT_RE.test(p.charAt(0))) return { rootLength: 1, prefix: "\\", rootPath: "\\" }; - return { rootLength: 0, prefix: "", rootPath: "" }; - } - function win32IsAbsolute(p) { - if (/^[\\/]{2}/.test(p)) return true; // UNC: \\server\share - if (/^[a-zA-Z]:[\\/]/.test(p)) return true; // drive-qualified: C:\... - if (/^[\\/]/.test(p)) return true; // drive-relative: \foo - return false; - } - var win32 = makePathImpl("\\", ";", WIN32_SPLIT_RE, win32IsAbsolute, win32Root); + // Native too (js_path_win_* in src/node.c). This was the last of path + // still written in JavaScript, and the last place here that walked a + // string with a regexp. + var win32 = { + sep: "\\", + delimiter: ";", + isAbsolute: __sxnWinIsAbsolute, + normalize: __sxnWinNormalize, + join: __sxnWinJoin, + resolve: __sxnWinResolve, + dirname: __sxnWinDirname, + basename: __sxnWinBasename, + extname: __sxnWinExtname, + relative: __sxnWinRelative, + }; var path = __sxnIsWindows ? win32 : posix; path.posix = posix; @@ -457,6 +295,14 @@ delete globalThis.__sxnPosixBasename; delete globalThis.__sxnPosixExtname; delete globalThis.__sxnPosixRelative; + delete globalThis.__sxnWinJoin; + delete globalThis.__sxnWinResolve; + delete globalThis.__sxnWinNormalize; + delete globalThis.__sxnWinIsAbsolute; + delete globalThis.__sxnWinDirname; + delete globalThis.__sxnWinBasename; + delete globalThis.__sxnWinExtname; + delete globalThis.__sxnWinRelative; // ---------------- process ---------------- function Process() { @@ -508,7 +354,9 @@ // doubling it. const stripNL = (s) => (s.endsWith("\n") ? s.slice(0, -1) : s); process.stdout = makeStdio(1, (t) => { if (t) console.log(stripNL(t)); }); - process.stderr = makeStdio(2, (t) => { if (t) console.error(stripNL(t)); }); + // Straight to fd 2 -- console.error is itself built on __sxnWriteStderr, so + // routing through it would be a cycle. + process.stderr = makeStdio(2, (t) => { if (t) __sxnWriteStderr(t); }); process.stdin = { fd: 0, isTTY: false, readable: false, on() { return this; }, once() { return this; }, off() { return this; }, resume() { return this; }, pause() { return this; }, @@ -527,20 +375,32 @@ // resolve back to this executable, so there is nothing to pass but the // module object it should fill in. if (typeof __sxnDlopen === "function") process.dlopen = __sxnDlopen; - process.emitWarning = function (w) { console.error("Warning: " + (w && w.message ? w.message : w)); }; + // Node prints ": " and callers lean on the name to tell + // warnings apart -- MaxListenersExceededWarning is the one this runtime + // raises itself, from sxn_ee_check_max_listeners. + process.emitWarning = function (w) { + const message = w && w.message ? w.message : String(w); + const name = w && w.name && w.name !== "Error" ? w.name + ": " : ""; + console.error("Warning: " + name + message); + }; process.uptime = function () { return performance.now() / 1000; }; - process.pid = 0; - process.cwd = function () { return __sxnCwd(); }; + // The real one: a program that runs several copies of itself -- which is + // how this runtime uses more than one core -- has nothing else to tell them + // apart by in a log. + process.pid = typeof __sxnPid === "number" ? __sxnPid : 0; + process.cwd = __sxnCwd; + // Node has chdir, and this runtime now needs one anyway: it is what tells + // the cached cwd it is stale. + process.chdir = __sxnChdir; process.exit = function (code) { __sxnExit(code === undefined ? 0 : code); }; // A genuine job-queue microtask (queueMicrotask is itself a thin JS_EnqueueJob // wrapper built into quickjs.c), not a timer -- so nextTick callbacks always // run before any subsequently-scheduled timer/I-O callback, matching Node's // "runs before the event loop continues" contract for the cases this runtime // supports. - process.nextTick = function (fn) { - var args = Array.prototype.slice.call(arguments, 1); - queueMicrotask(function () { fn.apply(undefined, args); }); - }; + // Native (js_next_tick in src/node.c): this copied `arguments` into an + // array and built a closure over it for every tick. + process.nextTick = __sxnNextTick; // process.on('SIGINT'/'SIGTERM', ...) only arms the native libuv signal // watcher (see __sxnWatchSignal in src/node.c) the first time a listener @@ -573,6 +433,22 @@ function wantsText(encoding) { return typeof encoding === "string" || (encoding && typeof encoding.encoding === "string"); } + // A file's size, kind and times. The numbers come from libuv; the methods + // and the Date fields are the shape Node hands back. + var S_IFMT = 0o170000, S_IFREG = 0o100000, S_IFDIR = 0o040000, S_IFLNK = 0o120000; + var S_IFCHR = 0o020000, S_IFBLK = 0o060000, S_IFIFO = 0o010000, S_IFSOCK = 0o140000; + // Native: __sxnStat fills the object itself, on this prototype, rather + // than handing back a plain one whose every field was then copied across + // by a for-in loop here. + function Stats() {} + Stats.prototype.isFile = function () { return (this.mode & S_IFMT) === S_IFREG; }; + Stats.prototype.isDirectory = function () { return (this.mode & S_IFMT) === S_IFDIR; }; + Stats.prototype.isSymbolicLink = function () { return (this.mode & S_IFMT) === S_IFLNK; }; + Stats.prototype.isCharacterDevice = function () { return (this.mode & S_IFMT) === S_IFCHR; }; + Stats.prototype.isBlockDevice = function () { return (this.mode & S_IFMT) === S_IFBLK; }; + Stats.prototype.isFIFO = function () { return (this.mode & S_IFMT) === S_IFIFO; }; + Stats.prototype.isSocket = function () { return (this.mode & S_IFMT) === S_IFSOCK; }; + var fs = { readFileSync: function (path, encoding) { var bytes = __sxnReadFileSync(path); @@ -581,18 +457,56 @@ }, writeFileSync: globalThis.__sxnWriteFileSync, existsSync: globalThis.__sxnExistsSync, + statSync: function (path) { return __sxnStat(path, true, Stats.prototype); }, + lstatSync: function (path) { return __sxnStat(path, false, Stats.prototype); }, + Stats: Stats, + // The whole file, handed to a Readable in one chunk. Enough for serving + // a file, which is what this exists for; it is not a window onto a file + // too large to hold. + createReadStream: function (path, options) { + var stream = new Readable(); + queueMicrotask(function () { + try { + var bytes = __sxnReadFileSync(path); + var start = (options && options.start) || 0; + var end = options && options.end !== undefined ? options.end + 1 : bytes.byteLength; + var slice = bytes.subarray(start, end); + var encoding = options && (typeof options === "string" ? options : options.encoding); + stream.push(encoding ? new TextDecoder().decode(slice) + : Buffer.from(slice.buffer, slice.byteOffset, slice.byteLength)); + stream.push(null); + } catch (e) { + stream.emit("error", e); + } + }); + return stream; + }, }; + // The flag numbers come from this platform's headers (js_fs_constants in + // src/network.c), which is the only way to get O_CREAT right everywhere. + fs.constants = __sxnFsConstants(); globalThis.__sxnFs = fs; delete globalThis.__sxnWriteFileSync; delete globalThis.__sxnExistsSync; var fsPromises = { + // The same native read the synchronous side uses. This went through + // Sxn.file().text(), which decodes the file as UTF-8 and then had to + // encode it back to bytes -- so any byte that is not valid UTF-8 came + // back as the replacement character, and a binary file was corrupted. readFile: function (path, encoding) { - return Sxn.file(path).text().then(function (text) { - return wantsText(encoding) ? text : Buffer.from(new TextEncoder().encode(text).buffer); - }); + try { return Promise.resolve(fs.readFileSync(path, encoding)); } + catch (e) { return Promise.reject(e); } }, writeFile: __sxnWriteFileAsync, + stat: function (path) { + try { return Promise.resolve(__sxnStat(path, true, Stats.prototype)); } + catch (e) { return Promise.reject(e); } + }, + lstat: function (path) { + try { return Promise.resolve(__sxnStat(path, false, Stats.prototype)); } + catch (e) { return Promise.reject(e); } + }, }; globalThis.__sxnFsPromises = fsPromises; @@ -613,6 +527,7 @@ EE.call(this); options = options || {}; this._buf = []; + this._bufAt = 0; // read cursor: shift() on a long queue copies it this._flowing = false; this._ended = false; this._endEmitted = false; @@ -645,29 +560,41 @@ this._buf.push(chunk); if (this._flowing) drainReadable(this); else this.emit("readable"); - return this._buf.length < 16; // a coarse high-water mark + return this._buf.length - this._bufAt < 16; // a coarse high-water mark }; + // Chunks leave through a cursor rather than shift(), which copies the + // whole queue down by one every time and made draining a long one cost + // its own length squared. + function takeChunk(r) { + const chunk = r._buf[r._bufAt]; + r._buf[r._bufAt++] = undefined; + if (r._bufAt === r._buf.length) { r._buf.length = 0; r._bufAt = 0; } + return chunk; + } + function bufferedCount(r) { return r._buf.length - r._bufAt; } function drainReadable(r) { - while (r._flowing && r._buf.length > 0) r.emit("data", r._buf.shift()); + while (r._flowing && bufferedCount(r) > 0) r.emit("data", takeChunk(r)); maybeEndReadable(r); if (r._flowing && !r._ended) r._read(); } function maybeEndReadable(r) { - if (r._ended && r._buf.length === 0 && !r._endEmitted) { + if (r._ended && bufferedCount(r) === 0 && !r._endEmitted) { r._endEmitted = true; r.readable = false; queueMicrotask(() => { r.emit("end"); r.emit("close"); }); } } Readable.prototype.read = function () { - if (this._buf.length === 0) { this._read(); } - if (this._buf.length === 0) { maybeEndReadable(this); return null; } + if (bufferedCount(this) === 0) { this._read(); } + if (bufferedCount(this) === 0) { maybeEndReadable(this); return null; } let c; - if (this._readableObjectMode || this._encoding || this._buf.length === 1) { - c = this._buf.shift(); + if (this._readableObjectMode || this._encoding || bufferedCount(this) === 1) { + c = takeChunk(this); } else { // Node hands back everything buffered as one Buffer. - c = Buffer.concat(this._buf.splice(0)); + c = Buffer.concat(this._buf.splice(this._bufAt)); + this._buf.length = 0; + this._bufAt = 0; } maybeEndReadable(this); return c; @@ -686,19 +613,10 @@ queueMicrotask(() => { if (err) this.emit("error", err); this.emit("close"); }); return this; }; - Readable.prototype.pipe = function (dest, options) { - const onData = (chunk) => { if (dest.write(chunk) === false) this.pause(); }; - const onDrain = () => this.resume(); - const onEnd = () => { if (!options || options.end !== false) dest.end(); }; - const onError = (e) => dest.emit("error", e); - this.on("data", onData); - dest.on("drain", onDrain); - this.on("end", onEnd); - this.on("error", onError); - (this._pipes || (this._pipes = [])).push({ dest, onData, onDrain, onEnd, onError }); - this.resume(); - return dest; - }; + // Native (js_stream_pipe in src/node.c): four closures per pipe, one per + // event, became four C functions sharing the source and the destination. + // The record kept for unpipe has the same shape it always had. + Readable.prototype.pipe = __sxnPipe; // Node's readable.unpipe([dest]). finalhandler calls it before draining a // request it is about to answer, so it has to exist even on a stream that // was never piped anywhere. @@ -729,7 +647,7 @@ const self = this; return { next() { - if (self._buf.length > 0) return Promise.resolve({ value: self._buf.shift(), done: false }); + if (bufferedCount(self) > 0) return Promise.resolve({ value: takeChunk(self), done: false }); if (self._ended) return Promise.resolve({ value: undefined, done: true }); return new Promise((resolve, reject) => { const onData = (c) => { cleanup(); resolve({ value: c, done: false }); }; @@ -821,14 +739,11 @@ if (cb) cb(e); return false; } - let sync = true, backpressure = false; - this._write(chunk, enc || "utf8", (err) => { - if (err) { this.emit("error", err); if (cb) cb(err); return; } - if (cb) cb(null); - if (backpressure) queueMicrotask(() => this.emit("drain")); - }); - sync = false; void sync; - return !backpressure; + // Native (sxn_write_done in src/node.c): the callback _write is handed + // was a JavaScript closure per chunk. An error still reaches the + // stream's 'error' listeners whether or not a callback was passed. + this._write(chunk, enc || "utf8", __sxnWriteCallback(this, cb)); + return true; }; Writable.prototype.cork = function () { this._corked++; }; Writable.prototype.uncork = function () { if (this._corked) this._corked--; }; @@ -980,44 +895,37 @@ Readable.call(this, {}); this.method = raw.method || "GET"; this.url = raw.url || "/"; - this.headers = {}; - for (const k of Object.keys(raw.headers || {})) - this.headers[k.toLowerCase()] = raw.headers[k]; + // Native (js_http_headers in src/node.c): lowercase every name and + // flatten into rawHeaders in one pass, once per request. + const named = __sxnHttpHeaders(raw.headers); + this.headers = named.headers; this.httpVersion = "1.1"; - this.rawHeaders = []; - for (const k of Object.keys(this.headers)) this.rawHeaders.push(k, this.headers[k]); + this.rawHeaders = named.rawHeaders; // on-finished reads socket.readable and `complete` to decide whether a // request is spent; body-parser skips parsing when it says yes, so both // have to describe a request whose body has not been read yet. The socket // is a real emitter because on-finished subscribes to its 'error' and // 'close', and a plain object had no on() for it to call. - this.socket = Object.assign(new EE(), { - remoteAddress: "127.0.0.1", remotePort: 0, localAddress: "127.0.0.1", - encrypted: false, readable: true, writable: true, destroyed: false, - setTimeout() { return this; }, setNoDelay() { return this; }, - setKeepAlive() { return this; }, - destroy() { this.destroyed = true; this.readable = this.writable = false; - this.emit("close"); return this; }, - end() { return this.destroy(); }, - }); + // Native (js_http_socket in src/node.c): the shape is the same every + // request, so the prototype is built once rather than eleven properties + // being copied onto a fresh emitter each time. + this.socket = __sxnHttpSocket(); this.connection = this.socket; this.complete = false; - this.once("end", () => { this.complete = true; }); + // Native, and shared: this was an arrow function per request. + this.once("end", __sxnHttpComplete); // The body is already off the wire, but it must not be pushed before the // consumer attaches: body-parser adds its 'data' listener after the // handler returns, and an eagerly-ended stream would hand it nothing. // Pushing from _read defers until something actually reads. - let sent = false; - const body = raw.body; - this._read = () => { - if (sent) return; - sent = true; - if (body !== undefined && body !== null && body !== "") this.push(body); - this.push(null); - }; + this._rawBody = raw.body; + this._bodySent = false; } IncomingMessage.prototype = Object.create(Readable.prototype); IncomingMessage.prototype.constructor = IncomingMessage; + // Native (js_http_read_body in src/node.c), and on the prototype rather + // than a closure built per request. + IncomingMessage.prototype._read = __sxnHttpReadBody; function ServerResponse(settle) { Writable.call(this, {}); @@ -1031,17 +939,14 @@ } ServerResponse.prototype = Object.create(Writable.prototype); ServerResponse.prototype.constructor = ServerResponse; - ServerResponse.prototype.setHeader = function (name, value) { - this._headers[String(name).toLowerCase()] = value; - return this; - }; - ServerResponse.prototype.getHeader = function (name) { return this._headers[String(name).toLowerCase()]; }; - ServerResponse.prototype.getHeaders = function () { return Object.assign({}, this._headers); }; - ServerResponse.prototype.getHeaderNames = function () { return Object.keys(this._headers); }; - ServerResponse.prototype.hasHeader = function (name) { - return Object.prototype.hasOwnProperty.call(this._headers, String(name).toLowerCase()); - }; - ServerResponse.prototype.removeHeader = function (name) { delete this._headers[String(name).toLowerCase()]; }; + // Native (js_header_op in src/node.c): each of these lowercases the name + // first, which is a scan over a short string rather than a builtin call. + ServerResponse.prototype.setHeader = __sxnSetHeader; + ServerResponse.prototype.getHeader = __sxnGetHeader; + ServerResponse.prototype.getHeaders = __sxnGetHeaders; + ServerResponse.prototype.getHeaderNames = __sxnGetHeaderNames; + ServerResponse.prototype.hasHeader = __sxnHasHeader; + ServerResponse.prototype.removeHeader = __sxnRemoveHeader; ServerResponse.prototype.writeHead = function (status, reasonOrHeaders, maybeHeaders) { this.statusCode = status; let headers = maybeHeaders; @@ -1065,19 +970,20 @@ if (chunk !== undefined && chunk !== null) this._chunks.push(chunk); this.finished = true; this.headersSent = true; - // Concatenate once: text chunks join, binary chunks merge into one array. - let body; - const anyBinary = this._chunks.some((c) => c instanceof Uint8Array || c instanceof ArrayBuffer); - if (anyBinary) { - const parts = this._chunks.map((c) => + // Concatenate once. Native (js_join_chunks in src/node.c) answers the + // cases every real response is -- nothing written, one string, all bytes + // -- and hands back undefined for a mix, where joining strings is the + // engine's own job. + let body = __sxnJoinChunks(this._chunks); + if (body === undefined) { + const anyBinary = this._chunks.some((c) => c instanceof Uint8Array || c instanceof ArrayBuffer); + if (!anyBinary) body = this._chunks.map((c) => String(c)).join(""); + else { + body = __sxnConcatBytes(this._chunks.map((c) => c instanceof Uint8Array ? c : c instanceof ArrayBuffer ? new Uint8Array(c) - : new TextEncoder().encode(String(c))); - let total = 0; for (const p of parts) total += p.length; - body = new Uint8Array(total); - let at = 0; for (const p of parts) { body.set(p, at); at += p.length; } - } else { - body = this._chunks.map((c) => String(c)).join(""); + : new TextEncoder().encode(String(c)))); + } } this._settle({ statusCode: this.statusCode, headers: this._headers, body }); if (cb) queueMicrotask(cb); @@ -1173,45 +1079,13 @@ // Address validation is what frameworks import this for -- Express uses // net.isIP when parsing X-Forwarded-For. Real sockets are not implemented, // and say so rather than pretending to connect. - function isIPv4(s) { - if (typeof s !== "string") return false; - const parts = s.split("."); - if (parts.length !== 4) return false; - return parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255 && - (p === "0" || p[0] !== "0")); - } - function isIPv6(s) { - if (typeof s !== "string" || s.indexOf(":") < 0) return false; - // A zone index (fe80::1%eth0) names an interface, not part of the - // address; Node accepts it and so does this. - const pct = s.indexOf("%"); - if (pct >= 0) s = s.slice(0, pct); - // At most one "::", and every group is 1-4 hex digits. A trailing IPv4 - // form is allowed, as in ::ffff:127.0.0.1. - const dbl = s.split("::"); - if (dbl.length > 2) return false; - let tail = s; - let v4extra = 0; - const lastColon = s.lastIndexOf(":"); - const maybeV4 = s.slice(lastColon + 1); - if (maybeV4.indexOf(".") >= 0) { - if (!isIPv4(maybeV4)) return false; - tail = s.slice(0, lastColon); - v4extra = 2; // an embedded IPv4 fills two groups - } - const groups = tail.split(":").filter((g, i, a) => !(g === "" && i > 0 && i < a.length - 1) || true); - let count = 0; - for (const g of tail.split(":")) { - if (g === "") continue; - if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return false; - count++; - } - void groups; - const total = count + v4extra; - return dbl.length === 2 ? total <= 8 : total === 8; - } + // Native (js_net_is_ip in src/node.c): the system's own address parser, + // rather than two regexps and a split per call. + const isIPv4 = (s) => __sxnIsIP(s) === 4; + const isIPv6 = (s) => __sxnIsIP(s) === 6; + const net = { - isIP: (s) => (isIPv4(s) ? 4 : isIPv6(s) ? 6 : 0), + isIP: __sxnIsIP, isIPv4, isIPv6, Socket: function Socket() { throw new Error("net.Socket is not implemented"); }, Server: function Server() { throw new Error("net.Server is not implemented; use node:http"); }, @@ -1226,36 +1100,29 @@ // and a constant-time compare. Digests come from the same OpenSSL binding // WebCrypto uses; HMAC is the standard construction over it, which needs no // second binding and is exactly what the RFC specifies. - const HASH_BLOCK = { md5: 64, sha1: 64, sha224: 64, sha256: 64, sha384: 128, sha512: 128 }; const cryptoToBytes = (d, enc) => { if (typeof d === "string") { - if (enc === "hex") { - const out = new Uint8Array(d.length >> 1); - for (let i = 0; i < out.length; i++) out[i] = parseInt(d.substr(i * 2, 2), 16); - return out; - } - if (enc === "base64") return Uint8Array.from(atob(d), (c) => c.charCodeAt(0)); + // Buffer's readers are the native ones; this used to parse hex a byte + // at a time with parseInt and read base64 through atob. + if (enc && enc !== "utf8" && enc !== "utf-8") return bufferBytesFromString(d, enc); return new TextEncoder().encode(d); } if (d instanceof ArrayBuffer) return new Uint8Array(d); if (ArrayBuffer.isView(d)) return new Uint8Array(d.buffer, d.byteOffset, d.byteLength); throw new TypeError("expected a string, Buffer or TypedArray"); }; + // Uint8Array's own toHex/toBase64 are native; building the hex by hand cost + // a string per byte, an array and a join -- which was most of the time a + // digest took once the digest itself was C. const encodeDigest = (bytes, encoding) => { if (!encoding || encoding === "buffer") return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength); - if (encoding === "hex") - return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); - if (encoding === "base64") - return btoa(String.fromCharCode(...bytes)); + if (encoding === "hex") return bytes.toHex(); + if (encoding === "base64") return bytes.toBase64(); + if (encoding === "base64url") return bytes.toBase64({ alphabet: "base64url", omitPadding: true }); throw new TypeError("unsupported digest encoding: " + encoding); }; - const concatBytes = (parts) => { - let total = 0; for (const p of parts) total += p.length; - const out = new Uint8Array(total); - let at = 0; for (const p of parts) { out.set(p, at); at += p.length; } - return out; - }; + const concatBytes = __sxnConcatBytes; function Hash(algorithm) { this._algo = String(algorithm).toLowerCase(); @@ -1266,7 +1133,8 @@ return this; }; Hash.prototype.digest = function (encoding) { - return encodeDigest(__sxnDigest(this._algo, concatBytes(this._parts)), encoding); + const data = this._parts.length === 1 ? this._parts[0] : concatBytes(this._parts); + return encodeDigest(__sxnDigest(this._algo, data), encoding); }; Hash.prototype.copy = function () { const h = new Hash(this._algo); @@ -1274,26 +1142,21 @@ return h; }; + // Native (sxn_hmac in src/network.c): OpenSSL's own HMAC, rather than two + // padded key buffers and three digest calls built here. function Hmac(algorithm, key) { - const algo = String(algorithm).toLowerCase(); - const block = HASH_BLOCK[algo.replace(/-/g, "")] || 64; - let k = cryptoToBytes(key); - if (k.length > block) k = __sxnDigest(algo, k); - const padded = new Uint8Array(block); - padded.set(k); - const ipad = new Uint8Array(block), opad = new Uint8Array(block); - for (let i = 0; i < block; i++) { ipad[i] = padded[i] ^ 0x36; opad[i] = padded[i] ^ 0x5c; } - this._algo = algo; - this._opad = opad; - this._parts = [ipad]; + this._algo = String(algorithm).toLowerCase(); + this._key = cryptoToBytes(key); + this._parts = []; } Hmac.prototype.update = function (data, enc) { this._parts.push(cryptoToBytes(data, enc)); return this; }; Hmac.prototype.digest = function (encoding) { - const inner = __sxnDigest(this._algo, concatBytes(this._parts)); - return encodeDigest(__sxnDigest(this._algo, concatBytes([this._opad, inner])), encoding); + // One update is the usual case, and then there is nothing to join. + const data = this._parts.length === 1 ? this._parts[0] : concatBytes(this._parts); + return encodeDigest(__sxnHmac(this._algo, this._key, data), encoding); }; const nodeCrypto = { @@ -1315,13 +1178,9 @@ return min + (v % range); }, // Compares in time independent of where the first difference is. - timingSafeEqual(a, b) { - const x = cryptoToBytes(a), y = cryptoToBytes(b); - if (x.length !== y.length) throw new RangeError("input length mismatch"); - let diff = 0; - for (let i = 0; i < x.length; i++) diff |= x[i] ^ y[i]; - return diff === 0; - }, + // Native: a comparison that takes the same time either way is not + // something a JavaScript loop can promise. + timingSafeEqual: (a, b) => __sxnTimingSafeEqual(cryptoToBytes(a), cryptoToBytes(b)), getHashes: () => ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"], constants: {}, webcrypto: globalThis.crypto, @@ -1402,10 +1261,7 @@ transform(chunk, enc, cb) { parts.push(toBytes(chunk)); cb(); }, flush(cb) { try { - let total = 0; for (const p of parts) total += p.length; - const joined = new Uint8Array(total); - let at = 0; for (const p of parts) { joined.set(p, at); at += p.length; } - cb(null, syncFn(joined, options)); + cb(null, syncFn(__sxnConcatBytes(parts), options)); } catch (e) { cb(e); } }, })); @@ -1437,15 +1293,24 @@ // across a chunk boundary -- which is the entire reason it exists. function StringDecoder(encoding) { this.encoding = (encoding || "utf8").toLowerCase(); - this._dec = new TextDecoder(this.encoding === "utf8" ? "utf-8" : this.encoding); + this._tail = null; + // utf-8 is native (js_decode_chunk in src/node.c); the other encodings + // keep the TextDecoder, which is where they came from. + this._dec = (this.encoding === "utf8" || this.encoding === "utf-8") + ? null : new TextDecoder(this.encoding); } StringDecoder.prototype.write = function (buf) { if (typeof buf === "string") return buf; - return this._dec.decode(buf, { stream: true }); + if (this._dec) return this._dec.decode(buf, { stream: true }); + return __sxnDecodeChunk(this, buf); }; StringDecoder.prototype.end = function (buf) { let out = buf ? this.write(buf) : ""; - out += this._dec.decode(); + if (this._dec) return out + this._dec.decode(); + // Anything still held back was never going to complete. It is the start + // of one character, however many bytes of it arrived, so Node emits one + // replacement character for it. + if (this._tail) { out += "\ufffd"; this._tail = null; } return out; }; globalThis.__sxnStringDecoder = { StringDecoder }; @@ -1496,151 +1361,50 @@ const moduleModule = Object.assign(Module, { Module, createRequire: (from) => __sxnMakeRequire(String(from)), - builtinModules: ["assert","buffer","events","fs","http","os","path","process", - "querystring","stream","string_decoder","timers","tty","url","util"], - isBuiltin: (n) => moduleModule.builtinModules.includes(String(n).replace(/^node:/, "")), + // Native, and read off the table require() itself uses (js_builtin_names + // in src/node.c): the list here used to be written by hand and had fallen + // short of several modules that do resolve. + builtinModules: __sxnBuiltinNames(), + isBuiltin: __sxnIsBuiltin, }); globalThis.__sxnModule = moduleModule; // CommonJS reaches the builtins through require(), which is synchronous, so - // it cannot go through import(). Every builtin is already a plain object on - // a global; this maps a specifier to it, with or without the node: prefix. - globalThis.__sxnBuiltinRequire = function (specifier) { - const name = String(specifier).replace(/^node:/, ""); - const table = { - buffer: { Buffer, default: Buffer }, - events: globalThis.__sxnEventEmitter, - path: globalThis.__sxnPath, - process: globalThis.process, - fs: globalThis.__sxnFs, - "fs/promises": globalThis.__sxnFsPromises, - util: globalThis.__sxnUtil, - os: globalThis.__sxnOs, - url: globalThis.__sxnUrl, - querystring: globalThis.__sxnQuerystring, - assert: globalThis.__sxnAssert, - "assert/strict": globalThis.__sxnAssert, - stream: globalThis.__sxnStream, - "stream/promises": globalThis.__sxnStream && globalThis.__sxnStream.promises, - http: globalThis.__sxnHttp, - tty: globalThis.__sxnTty, - string_decoder: globalThis.__sxnStringDecoder, - timers: globalThis.__sxnTimers, - "timers/promises": globalThis.__sxnTimers && globalThis.__sxnTimers.promises, - perf_hooks: globalThis.__sxnPerfHooks, - module: globalThis.__sxnModule, - zlib: globalThis.__sxnZlib, - crypto: globalThis.__sxnCrypto, - net: globalThis.__sxnNet, - }; - const m = table[name]; - if (m === undefined) { - const e = new Error("Cannot find module '" + specifier + "'"); - e.code = "MODULE_NOT_FOUND"; - throw e; - } - // events exports the constructor itself, and Node lets you reach the - // named helpers off it either way. - return m; - }; - globalThis.__sxnIsBuiltin = function (specifier) { - try { globalThis.__sxnBuiltinRequire(specifier); return true; } catch { return false; } - }; + // it cannot go through import(). The mapping from specifier to builtin is + // native (sxn_builtin_lookup in src/node.c), where the table is static + // rather than an object rebuilt on every require() call. // ---------------- node:util ---------------- // The parts packages actually import: promisify, callbackify, inherits, // format, deprecate, and the types guards. inspect is a readable // approximation, not Node's exact formatter. - function inspect(v, opts, depth) { - opts = opts || {}; - const max = opts.depth === undefined ? 2 : opts.depth; - const seen = opts._seen || new Set(); - depth = depth || 0; - const t = typeof v; - if (v === null) return "null"; - if (t === "string") return depth === 0 && !opts.quoteStrings ? v : JSON.stringify(v); - if (t === "number" || t === "boolean" || t === "undefined") return String(v); - if (t === "bigint") return String(v) + "n"; - if (t === "symbol") return v.toString(); - if (t === "function") return "[Function: " + (v.name || "anonymous") + "]"; - if (v instanceof Error) return v.stack || (v.name + ": " + v.message); - if (v instanceof Date) return v.toISOString(); - if (v instanceof RegExp) return String(v); - if (seen.has(v)) return "[Circular *1]"; - if (depth > max) return Array.isArray(v) ? "[Array]" : "[Object]"; - seen.add(v); - const sub = Object.assign({}, opts, { _seen: seen, quoteStrings: true }); - let out; - if (Array.isArray(v)) { - out = "[ " + v.map((e) => inspect(e, sub, depth + 1)).join(", ") + " ]"; - if (v.length === 0) out = "[]"; - } else if (v instanceof Map) { - out = "Map(" + v.size + ") {" + (v.size ? " " + [...v].map(([k, val]) => - inspect(k, sub, depth + 1) + " => " + inspect(val, sub, depth + 1)).join(", ") + " " : "") + "}"; - } else if (v instanceof Set) { - out = "Set(" + v.size + ") {" + (v.size ? " " + [...v].map((e) => - inspect(e, sub, depth + 1)).join(", ") + " " : "") + "}"; - } else if (ArrayBuffer.isView(v)) { - out = v.constructor.name + "(" + v.length + ") [ " + Array.from(v).join(", ") + " ]"; - } else { - const keys = Object.keys(v); - out = keys.length === 0 ? "{}" : "{ " + keys.map((k) => - (/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)) + ": " + - inspect(v[k], sub, depth + 1)).join(", ") + " }"; - } - seen.delete(v); - return out; - } - - function format(...args) { - if (typeof args[0] !== "string") return args.map((a) => inspect(a)).join(" "); - // With nothing to substitute, Node returns the string untouched -- even - // "%%" stays as written. - if (args.length === 1) return args[0]; - let i = 1; - let out = args[0].replace(/%[sdifjoOc%]/g, (m) => { - if (m === "%%") return "%"; - if (i >= args.length) return m; - const a = args[i++]; - switch (m) { - case "%s": return typeof a === "string" ? a : inspect(a); - case "%d": case "%f": return typeof a === "bigint" ? a + "n" : Number(a).toString(); - case "%i": return typeof a === "bigint" ? a + "n" : parseInt(a, 10).toString(); - case "%j": try { return JSON.stringify(a); } catch { return "[Circular]"; } - case "%o": case "%O": return inspect(a, { depth: 4 }); - case "%c": return ""; - default: return m; - } - }); - for (; i < args.length; i++) out += " " + (typeof args[i] === "string" ? args[i] : inspect(args[i])); - return out; - } + // Native (js_inspect in src/node.c). This was a recursive walk that built + // an options object per level, a mapped array per container and a joined + // string per level; the C one walks into a single buffer. It also answers + // "Invalid Date" where this used to throw out of toISOString. + const inspect = __sxnInspect; + + // Native (js_util_format in src/node.c): the scan and the substitution are + // string work. Only the cases that need inspect -- %s of something that is + // not a string, and %o/%O -- come back into JavaScript, which is why + // inspect is handed over as the first argument. + const format = (...args) => __sxnFormat(inspect, ...args); const util = { inspect, format, // Node keeps the custom-inspect symbol here. - promisify(fn) { - if (typeof fn !== "function") throw new TypeError("promisify expects a function"); - const wrapped = function (...args) { - return new Promise((resolve, reject) => { - fn.call(this, ...args, (err, ...values) => - err ? reject(err) : resolve(values.length > 1 ? values : values[0])); - }); - }; - Object.defineProperty(wrapped, "name", { value: fn.name, configurable: true }); - return wrapped; - }, - callbackify(fn) { - return function (...args) { - const cb = args.pop(); - Promise.resolve(fn.apply(this, args)).then((v) => cb(null, v), (e) => cb(e || new Error("rejected"))); - }; - }, - inherits(ctor, superCtor) { - Object.defineProperty(ctor, "super_", { value: superCtor, writable: true, configurable: true }); - Object.setPrototypeOf(ctor.prototype, superCtor.prototype); - }, + // Native (js_promisify in src/node.c): this spread the arguments, built + // a Promise with an executor closure and a callback closure inside it, + // per call. It also resolved with an array when a callback reported + // more than one value; Node keeps the first and drops the rest. + promisify: __sxnPromisify, + // Native (js_callbackify in src/node.c): this built two closures per + // call for the two halves of the promise, 1.32 microseconds to 0.84. + callbackify: __sxnCallbackify, + // Native (js_inherits in src/node.c): two property operations, 0.250 + // microseconds to 0.060. + inherits: __sxnInherits, deprecate(fn, msg) { let warned = false; return function (...args) { @@ -1668,38 +1432,8 @@ globalThis.__sxnUtil = util; // Structural equality, shared by util.isDeepStrictEqual and node:assert. - function deepEqual(a, b, strict) { - if (strict ? Object.is(a, b) : a == b) return true; - if (a === null || b === null || typeof a !== "object" || typeof b !== "object") { - return strict ? Object.is(a, b) : a == b; - } - if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false; - if (a instanceof Date) return a.getTime() === b.getTime(); - if (a instanceof RegExp) return String(a) === String(b); - if (Array.isArray(a) !== Array.isArray(b)) return false; - if (ArrayBuffer.isView(a)) { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; - return true; - } - if (a instanceof Map) { - if (a.size !== b.size) return false; - for (const [k, v] of a) { if (!b.has(k) || !deepEqual(v, b.get(k), strict)) return false; } - return true; - } - if (a instanceof Set) { - if (a.size !== b.size) return false; - for (const v of a) if (!b.has(v)) return false; - return true; - } - const ka = Object.keys(a), kb = Object.keys(b); - if (ka.length !== kb.length) return false; - for (const k of ka) { - if (!Object.prototype.hasOwnProperty.call(b, k)) return false; - if (!deepEqual(a[k], b[k], strict)) return false; - } - return true; - } + // Native (sxn_deep_equal in src/node.c). + const deepEqual = (a, b, strict) => strict ? __sxnDeepEqual(a, b) : __sxnLooseDeepEqual(a, b); // ---------------- node:assert ---------------- function AssertionError(opts) { @@ -1733,69 +1467,817 @@ globalThis.__sxnAssert = assert; // ---------------- node:os ---------------- + // Answered by libuv. These used to be guesses -- "localhost" for the + // hostname, 0 for the memory sizes, an empty list of CPUs -- which is worse + // than not answering at all, because nothing can tell a stub from the + // truth. + const uname = () => __sxnOsUname(); const os = { EOL: "\n", platform: () => process.platform, arch: () => process.arch, - type: () => (process.platform === "darwin" ? "Darwin" : process.platform === "win32" ? "Windows_NT" : "Linux"), - release: () => "", - hostname: () => "localhost", - tmpdir: () => (process.env && (process.env.TMPDIR || process.env.TMP)) || "/tmp", - homedir: () => (process.env && process.env.HOME) || "/", + type: () => uname().sysname || (process.platform === "darwin" ? "Darwin" : process.platform === "win32" ? "Windows_NT" : "Linux"), + release: () => uname().release || "", + version: () => uname().version || "", + machine: () => uname().machine || process.arch, + hostname: () => __sxnOsHostname(), + tmpdir: () => __sxnOsTmpdir(), + homedir: () => __sxnOsHomedir(), endianness: () => "LE", - cpus: () => [], - totalmem: () => 0, - freemem: () => 0, - uptime: () => Math.floor(performance.now() / 1000), + cpus: () => __sxnOsCpus(), + availableParallelism: () => __sxnOsNumbers().parallelism, + networkInterfaces: () => __sxnOsInterfaces(), + totalmem: () => __sxnOsNumbers().totalmem, + freemem: () => __sxnOsNumbers().freemem, + loadavg: () => __sxnOsNumbers().loadavg, + uptime: () => Math.floor(__sxnOsNumbers().uptime), + userInfo: () => ({ + username: (process.env && (process.env.USER || process.env.USERNAME)) || "", + homedir: __sxnOsHomedir(), + shell: (process.env && process.env.SHELL) || null, + uid: -1, + gid: -1, + }), devNull: "/dev/null", + constants: { signals: {}, errno: {}, priority: {} }, }; globalThis.__sxnOs = os; // ---------------- node:querystring ---------------- + // Native (js_qs_* in src/node.c): pure string work with no state, which + // went through split(), a regexp for "+" and decodeURIComponent per part. const querystring = { - parse(str, sep, eq) { - const out = Object.create(null); - if (typeof str !== "string" || str.length === 0) return out; - for (const part of str.split(sep || "&")) { - if (!part) continue; - const i = part.indexOf(eq || "="); - const k = decodeURIComponent((i < 0 ? part : part.slice(0, i)).replace(/\+/g, " ")); - const v = i < 0 ? "" : decodeURIComponent(part.slice(i + 1).replace(/\+/g, " ")); - if (k in out) { if (Array.isArray(out[k])) out[k].push(v); else out[k] = [out[k], v]; } - else out[k] = v; - } - return out; - }, - stringify(obj, sep, eq) { - if (!obj || typeof obj !== "object") return ""; - const parts = []; - for (const k of Object.keys(obj)) { - const v = obj[k]; - const ek = encodeURIComponent(k); - if (Array.isArray(v)) for (const one of v) parts.push(ek + (eq || "=") + encodeURIComponent(one)); - else parts.push(ek + (eq || "=") + encodeURIComponent(v === undefined || v === null ? "" : v)); - } - return parts.join(sep || "&"); - }, - escape: encodeURIComponent, - unescape: decodeURIComponent, + parse: __sxnQsParse, + stringify: __sxnQsStringify, + escape: __sxnQsEscape, + unescape: __sxnQsUnescape, + decode: __sxnQsParse, + encode: __sxnQsStringify, }; globalThis.__sxnQuerystring = querystring; + delete globalThis.__sxnQsParse; + delete globalThis.__sxnQsStringify; + delete globalThis.__sxnQsEscape; + delete globalThis.__sxnQsUnescape; // ---------------- node:url ---------------- const url = { URL: globalThis.URL, URLSearchParams: globalThis.URLSearchParams, - fileURLToPath(u) { - const s = typeof u === "string" ? u : String(u); - if (!s.startsWith("file://")) throw new TypeError("must be a file: URL"); - return decodeURIComponent(s.slice(7).replace(/^localhost/, "")) || "/"; - }, - pathToFileURL(p) { - return new URL("file://" + encodeURI(String(p)).replace(/[?#]/g, encodeURIComponent)); - }, + // Native (js_file_url_to_path in src/node.c): a scheme check, a host + // check and a percent-decode, none of which needs a regexp. + fileURLToPath: (u) => __sxnFileUrlToPath(typeof u === "string" ? u : String(u)), + // The text is built native (js_path_to_file_url); the URL object it is + // handed to is the engine's own. + pathToFileURL: (p) => new URL(__sxnPathToFileUrl(String(p))), format: (u) => String(u), parse: (s) => { try { return new URL(s); } catch { return null; } }, }; globalThis.__sxnUrl = url; + // ---------------- the builtins past the first twenty ---------------- + // These are built on first use rather than at startup. Every program pays + // for what is above this line; almost none of them require node:dgram, and + // constructing all of this eagerly cost about 4% of a cold start. + const laterBuiltins = () => { + // ---------------- node:child_process ---------------- + // One native primitive (js_spawn_sync in src/network.c) runs a child to + // completion on a loop of its own. The synchronous calls are that call; the + // asynchronous ones are that call plus the events Node emits afterwards, so + // a child does not overlap with the rest of the program the way it does in + // Node. Anything that streams a long-running child's output as it arrives + // will see it all at once, at the end. + function spawnArgs(command, args, options) { + if (!Array.isArray(args)) { options = args; args = []; } + return [args || [], options || {}]; + } + function shellRun(command, options) { + const shell = (options && typeof options.shell === "string" && options.shell) || + (process.platform === "win32" ? "cmd.exe" : "/bin/sh"); + const flag = process.platform === "win32" ? "/d/s/c" : "-c"; + return __sxnSpawnSync(shell, [flag, command], options || {}); + } + function decodeOut(raw, options) { + const encoding = options && options.encoding; + if (encoding === "buffer" || encoding === null) return Buffer.from(raw); + return new TextDecoder().decode(raw); + } + function spawnError(result, command) { + const e = new Error("spawnSync " + command + " " + (result.errno || "failed")); + e.code = result.errno || "UNKNOWN"; + e.syscall = "spawnSync " + command; + e.path = command; + return e; + } + function spawnSync(command, args, options) { + const [list, opts] = spawnArgs(command, args, options); + const raw = opts.shell ? shellRun([command].concat(list).join(" "), opts) + : __sxnSpawnSync(command, list, opts); + const out = { + pid: raw.pid || 0, + status: raw.status === undefined ? null : raw.status, + signal: raw.signal === undefined ? null : raw.signal, + stdout: decodeOut(raw.stdout, opts), + stderr: decodeOut(raw.stderr, opts), + error: raw.error ? spawnError(raw, command) : undefined, + }; + out.output = [null, out.stdout, out.stderr]; + return out; + } + function checkedSync(result, command) { + if (result.error) throw result.error; + if (result.status !== 0) { + const e = new Error("Command failed: " + command + "\n" + + (typeof result.stderr === "string" ? result.stderr : "")); + e.status = result.status; + e.signal = result.signal; + e.stdout = result.stdout; + e.stderr = result.stderr; + throw e; + } + return result.stdout; + } + // The asynchronous forms: run the child, then deliver its output and its + // exit through the same events Node uses, one tick later. + function ChildProcess(result) { + EE.call(this); + this.pid = result.pid || 0; + this.exitCode = result.status === undefined ? null : result.status; + this.signalCode = result.signal || null; + this.killed = false; + this.stdin = new Writable({ write(c, e, cb) { cb(); } }); + this.stdout = new Readable({ read() {} }); + this.stderr = new Readable({ read() {} }); + } + inheritEE(ChildProcess); + ChildProcess.prototype.kill = function () { this.killed = true; return false; }; + ChildProcess.prototype.unref = function () { return this; }; + ChildProcess.prototype.ref = function () { return this; }; + function deliver(child, raw, opts, cb) { + process.nextTick(() => { + if (raw.error) { + child.emit("error", spawnError(raw, "child")); + if (cb) cb(spawnError(raw, "child"), decodeOut(raw.stdout, opts), decodeOut(raw.stderr, opts)); + return; + } + if (raw.stdout.length) child.stdout.push(Buffer.from(raw.stdout)); + if (raw.stderr.length) child.stderr.push(Buffer.from(raw.stderr)); + child.stdout.push(null); + child.stderr.push(null); + if (cb) { + const stdout = decodeOut(raw.stdout, opts), stderr = decodeOut(raw.stderr, opts); + let e = null; + if (raw.status !== 0) { + e = new Error("Command failed"); + e.code = raw.status; + } + cb(e, stdout, stderr); + } + child.emit("exit", raw.status === undefined ? null : raw.status, raw.signal || null); + child.emit("close", raw.status === undefined ? null : raw.status, raw.signal || null); + }); + return child; + } + const childProcess = { + spawnSync, + execFileSync(file, args, options) { + const [list, opts] = spawnArgs(file, args, options); + return checkedSync(spawnSync(file, list, opts), file); + }, + execSync(command, options) { + const opts = options || {}; + const raw = shellRun(command, opts); + return checkedSync({ + error: raw.error ? spawnError(raw, command) : undefined, + status: raw.status, signal: raw.signal, + stdout: decodeOut(raw.stdout, opts), stderr: decodeOut(raw.stderr, opts), + }, command); + }, + spawn(command, args, options) { + const [list, opts] = spawnArgs(command, args, options); + const raw = opts.shell ? shellRun([command].concat(list).join(" "), opts) + : __sxnSpawnSync(command, list, opts); + return deliver(new ChildProcess(raw), raw, opts, null); + }, + exec(command, options, cb) { + if (typeof options === "function") { cb = options; options = {}; } + const opts = options || {}; + const raw = shellRun(command, opts); + return deliver(new ChildProcess(raw), raw, opts, cb); + }, + execFile(file, args, options, cb) { + if (typeof args === "function") { cb = args; args = []; options = {}; } + else if (typeof options === "function") { cb = options; options = {}; } + const opts = options || {}; + const raw = __sxnSpawnSync(file, args || [], opts); + return deliver(new ChildProcess(raw), raw, opts, cb); + }, + fork() { throw new Error("child_process.fork is not supported: a child would need its own runtime"); }, + ChildProcess, + }; + globalThis.__sxnChildProcess = childProcess; + + // ---------------- node:dns ---------------- + // uv_getaddrinfo, resolved on this thread (js_dns_lookup in src/network.c). + // The callback forms hand the answer back on a later tick, so they compose + // like Node's, but the resolution itself blocks. There is no resolver + // beyond the system one: the record types libuv cannot answer throw. + function dnsLookup(hostname, options, cb) { + if (typeof options === "function") { cb = options; options = {}; } + const opts = typeof options === "number" ? { family: options } : (options || {}); + let list, error = null; + try { list = __sxnDnsLookup(hostname, opts.family || 0); } + catch (e) { error = e; list = []; } + process.nextTick(() => { + if (error) return cb(error); + if (!list.length) { + const e = new Error("getaddrinfo ENOTFOUND " + hostname); + e.code = "ENOTFOUND"; + return cb(e); + } + if (opts.all) return cb(null, list); + cb(null, list[0].address, list[0].family); + }); + } + function dnsResolve(family) { + return function (hostname, cb) { + dnsLookup(hostname, { family, all: true }, (e, list) => + e ? cb(e) : cb(null, list.map((a) => a.address))); + }; + } + function noResolver(kind) { + return function (hostname, cb) { + const e = new Error("dns.resolve" + kind + " is not supported: there is no resolver beyond the system one"); + e.code = "ENOTIMP"; + if (cb) return process.nextTick(() => cb(e)); + throw e; + }; + } + const dns = { + lookup: dnsLookup, + resolve4: dnsResolve(4), + resolve6: dnsResolve(6), + resolve(hostname, type, cb) { + if (typeof type === "function") { cb = type; type = "A"; } + if (type === "A") return dns.resolve4(hostname, cb); + if (type === "AAAA") return dns.resolve6(hostname, cb); + return noResolver(type)(hostname, cb); + }, + resolveMx: noResolver("Mx"), resolveTxt: noResolver("Txt"), + resolveSrv: noResolver("Srv"), resolveNs: noResolver("Ns"), + resolveCname: noResolver("Cname"), reverse: noResolver("Ptr"), + getServers: () => [], + setServers() { throw new Error("dns.setServers is not supported: resolution goes through the system resolver"); }, + ADDRCONFIG: 1024, V4MAPPED: 8, ALL: 16, + }; + const promisify1 = (fn) => (...a) => new Promise((res, rej) => + fn(...a, (e, v) => e ? rej(e) : res(v))); + dns.promises = { + lookup: (hostname, options) => new Promise((res, rej) => + dnsLookup(hostname, options || {}, (e, address, family) => + e ? rej(e) : res(typeof address === "string" ? { address, family } : address))), + resolve4: promisify1(dns.resolve4), + resolve6: promisify1(dns.resolve6), + resolve: promisify1(dns.resolve), + getServers: dns.getServers, + }; + dns.Resolver = function Resolver() { return Object.assign(Object.create(null), dns); }; + globalThis.__sxnDns = dns; + globalThis.__sxnDnsPromises = dns.promises; + + // ---------------- node:https ---------------- + // The same client node:http uses: the request goes out through the same + // native fetch, which speaks TLS. Only the default protocol differs. + // There is no https server, because Sxn.serve does not terminate TLS. + const https = { + request(options, cb) { + const opts = typeof options === "string" ? { url: options } : Object.assign({}, options); + if (!opts.url && !opts.protocol) opts.protocol = "https:"; + return http.request(opts, cb); + }, + get(options, cb) { const r = https.request(options, cb); r.end(); return r; }, + Agent: function Agent(options) { this.options = options || {}; }, + globalAgent: null, + createServer() { throw new Error("https.createServer is not supported: this runtime does not terminate TLS"); }, + Server: function Server() { throw new Error("https.Server is not supported: this runtime does not terminate TLS"); }, + }; + https.globalAgent = new https.Agent({}); + globalThis.__sxnHttps = https; + + // ---------------- node:tls / node:http2 ---------------- + // Named so that a `require` resolves and a feature check can fail cleanly, + // rather than dying on a missing module. TLS is only available as a client, + // through fetch and node:https; there is no socket to hand back. + const tls = { + connect() { throw new Error("tls.connect is not supported: use fetch or node:https"); }, + createServer() { throw new Error("tls.createServer is not supported: this runtime does not terminate TLS"); }, + TLSSocket: function TLSSocket() { throw new Error("tls.TLSSocket is not supported"); }, + Server: function Server() { throw new Error("tls.Server is not supported"); }, + createSecureContext: (options) => Object.assign({}, options), + rootCertificates: [], + DEFAULT_MIN_VERSION: "TLSv1.2", + DEFAULT_MAX_VERSION: "TLSv1.3", + }; + globalThis.__sxnTls = tls; + + const http2 = { + constants: { + HTTP2_HEADER_METHOD: ":method", HTTP2_HEADER_PATH: ":path", + HTTP2_HEADER_STATUS: ":status", HTTP2_HEADER_AUTHORITY: ":authority", + HTTP2_HEADER_SCHEME: ":scheme", HTTP2_HEADER_CONTENT_TYPE: "content-type", + }, + connect() { throw new Error("http2.connect is not supported: the client speaks HTTP/1.1"); }, + createServer() { throw new Error("http2.createServer is not supported: the server speaks HTTP/1.1"); }, + createSecureServer() { throw new Error("http2.createSecureServer is not supported: the server speaks HTTP/1.1"); }, + getDefaultSettings: () => ({}), + }; + globalThis.__sxnHttp2 = http2; + + // ---------------- node:stream/web ---------------- + // The Web Streams already in the global scope, under the names Node also + // publishes them under. Nothing is reimplemented here. + globalThis.__sxnStreamWeb = { + ReadableStream: globalThis.ReadableStream, + ReadableStreamDefaultReader: globalThis.ReadableStreamDefaultReader, + ReadableStreamBYOBReader: globalThis.ReadableStreamBYOBReader, + ReadableStreamDefaultController: globalThis.ReadableStreamDefaultController, + ReadableByteStreamController: globalThis.ReadableByteStreamController, + ReadableStreamBYOBRequest: globalThis.ReadableStreamBYOBRequest, + WritableStream: globalThis.WritableStream, + WritableStreamDefaultWriter: globalThis.WritableStreamDefaultWriter, + WritableStreamDefaultController: globalThis.WritableStreamDefaultController, + TransformStream: globalThis.TransformStream, + TransformStreamDefaultController: globalThis.TransformStreamDefaultController, + ByteLengthQueuingStrategy: globalThis.ByteLengthQueuingStrategy, + CountQueuingStrategy: globalThis.CountQueuingStrategy, + TextEncoderStream: globalThis.TextEncoderStream, + TextDecoderStream: globalThis.TextDecoderStream, + CompressionStream: globalThis.CompressionStream, + DecompressionStream: globalThis.DecompressionStream, + }; + + // ---------------- node:vm ---------------- + // The engine has one realm, so a "new context" is a function whose + // parameters are the sandbox's keys: the code sees those names, and writes + // to them come back out. It is not an isolated global. + const vm = { + runInThisContext(code, options) { + return (0, eval)(String(code)); + }, + runInNewContext(code, sandbox, options) { + const box = sandbox || {}; + const keys = Object.keys(box); + const fn = new Function(...keys, '"use strict"; return (' + "function(){" + String(code) + "}" + ")()"); + return fn(...keys.map((k) => box[k])); + }, + runInContext(code, contextifiedObject, options) { + return vm.runInNewContext(code, contextifiedObject, options); + }, + createContext: (sandbox) => sandbox || {}, + isContext: (o) => typeof o === "object" && o !== null, + compileFunction(code, params, options) { + return new Function(...(params || []), String(code)); + }, + Script: function Script(code) { + this.code = String(code); + this.runInThisContext = () => vm.runInThisContext(this.code); + this.runInNewContext = (sandbox) => vm.runInNewContext(this.code, sandbox); + this.runInContext = (sandbox) => vm.runInNewContext(this.code, sandbox); + }, + }; + globalThis.__sxnVm = vm; + + // ---------------- node:v8 ---------------- + // The numbers come from the engine's own allocator, not V8's, and the names + // are Node's. serialize/deserialize are JSON, which covers the plain data + // packages put through them and rejects what it cannot carry. + const v8 = { + getHeapStatistics() { + const m = Sxn.memoryUsage(); + return { + total_heap_size: m.mallocSize, used_heap_size: m.memoryUsed, + heap_size_limit: m.mallocSize, total_available_size: 0, + total_heap_size_executable: 0, total_physical_size: m.mallocSize, + malloced_memory: m.mallocSize, peak_malloced_memory: m.mallocSize, + does_zap_garbage: 0, number_of_native_contexts: 1, number_of_detached_contexts: 0, + }; + }, + getHeapSpaceStatistics: () => [], + setFlagsFromString() {}, + serialize(value) { return Buffer.from(JSON.stringify(value), "utf8"); }, + deserialize(buf) { return JSON.parse(Buffer.from(buf).toString("utf8")); }, + cachedDataVersionTag: () => 0, + }; + globalThis.__sxnV8 = v8; + + // ---------------- node:worker_threads / node:cluster ---------------- + // One JS thread, one process. Both modules answer the questions a library + // asks before it decides whether it is the main one -- which is most of + // what they are used for -- and throw where a second thread is required. + const workerThreads = { + isMainThread: true, + threadId: 0, + parentPort: null, + workerData: null, + resourceLimits: {}, + SHARE_ENV: Symbol("nodejs.worker_threads.SHARE_ENV"), + Worker: function Worker() { throw new Error("worker_threads.Worker is not supported: this runtime has one JS thread"); }, + MessageChannel: globalThis.MessageChannel, + MessagePort: globalThis.MessagePort, + BroadcastChannel: function BroadcastChannel() { throw new Error("BroadcastChannel is not supported: this runtime has one JS thread"); }, + markAsUntransferable() {}, + moveMessagePortToContext() { throw new Error("moveMessagePortToContext is not supported"); }, + receiveMessageOnPort: () => undefined, + setEnvironmentData() {}, + getEnvironmentData: () => undefined, + }; + globalThis.__sxnWorkerThreads = workerThreads; + + function Cluster() { EE.call(this); } + inheritEE(Cluster); + const cluster = new Cluster(); + cluster.isPrimary = true; + cluster.isMaster = true; + cluster.isWorker = false; + cluster.worker = null; + cluster.workers = {}; + cluster.settings = {}; + cluster.schedulingPolicy = 2; + cluster.setupPrimary = function () {}; + cluster.setupMaster = function () {}; + cluster.fork = function () { throw new Error("cluster.fork is not supported: this runtime does not fork"); }; + cluster.disconnect = function (cb) { if (cb) process.nextTick(cb); }; + globalThis.__sxnCluster = cluster; + + // ---------------- node:readline ---------------- + // Lines out of any readable stream, and the promise form of question(). + // Terminal editing -- history, completion, cursor keys -- is not here: + // stdin arrives as plain bytes. + function Interface(options) { + EE.call(this); + const opts = options || {}; + this.input = opts.input || process.stdin; + this.output = opts.output || process.stdout; + this.terminal = false; + this.closed = false; + this._pending = []; + this._rest = ""; + const self = this; + if (this.input && typeof this.input.on === "function") { + this.input.on("data", (chunk) => self._feed(String(chunk))); + this.input.on("end", () => self._end()); + if (typeof this.input.resume === "function") this.input.resume(); + } + } + inheritEE(Interface); + Interface.prototype._feed = function (text) { + const parts = (this._rest + text).split("\n"); + this._rest = parts.pop(); + for (const line of parts) { + const clean = line.endsWith("\r") ? line.slice(0, -1) : line; + const waiter = this._pending.shift(); + if (waiter) waiter(clean); + else this.emit("line", clean); + } + }; + Interface.prototype._end = function () { + if (this._rest) { this._feed("\n"); } + this.close(); + }; + Interface.prototype.question = function (query, cb) { + if (this.output && typeof this.output.write === "function") this.output.write(query); + this._pending.push(cb); + }; + Interface.prototype.prompt = function () {}; + Interface.prototype.write = function (text) { + if (this.output && typeof this.output.write === "function") this.output.write(text); + }; + Interface.prototype.setPrompt = function () {}; + Interface.prototype.pause = function () { return this; }; + Interface.prototype.resume = function () { return this; }; + Interface.prototype.close = function () { + if (this.closed) return; + this.closed = true; + this.emit("close"); + }; + Interface.prototype[Symbol.asyncIterator] = function () { + const lines = []; + let waiting = null, done = false; + this.on("line", (l) => { if (waiting) { const w = waiting; waiting = null; w({ value: l, done: false }); } else lines.push(l); }); + this.on("close", () => { done = true; if (waiting) { const w = waiting; waiting = null; w({ value: undefined, done: true }); } }); + return { + next() { + if (lines.length) return Promise.resolve({ value: lines.shift(), done: false }); + if (done) return Promise.resolve({ value: undefined, done: true }); + return new Promise((res) => { waiting = res; }); + }, + [Symbol.asyncIterator]() { return this; }, + }; + }; + const readline = { + Interface, + createInterface: (options, output) => + new Interface(options && options.read !== undefined ? { input: options, output } : options), + clearLine: () => true, clearScreenDown: () => true, + cursorTo: () => true, moveCursor: () => true, + emitKeypressEvents() {}, + promises: null, + }; + readline.promises = { + Interface, + createInterface(options, output) { + const rl = readline.createInterface(options, output); + const ask = rl.question.bind(rl); + rl.question = (query) => new Promise((res) => ask(query, res)); + return rl; + }, + }; + globalThis.__sxnReadline = readline; + globalThis.__sxnReadlinePromises = readline.promises; + + // ---------------- node:async_hooks ---------------- + // AsyncLocalStorage is real and is the reason this module is here: a store + // entered for a synchronous run, and kept across an await by binding it to + // the promise chain the callback returns. The hook API around it reports + // one execution context, because that is what a single loop with no async + // tracking can honestly say. + function AsyncLocalStorage() { this._store = undefined; this._entered = false; } + AsyncLocalStorage.prototype.run = function (store, callback, ...args) { + const previous = this._store, wasIn = this._entered; + this._store = store; + this._entered = true; + let async = false; + const restore = () => { this._store = previous; this._entered = wasIn; }; + try { + const out = callback(...args); + // A callback that returns a promise keeps the store until the promise + // settles. Anything else that runs while it is awaiting sees the store + // too, which is where this parts company with Node: there is no async + // context tracking underneath, only the promise chain handed back. + if (out && typeof out.then === "function") { + async = true; + return out.then((v) => { restore(); return v; }, (e) => { restore(); throw e; }); + } + return out; + } finally { + if (!async) restore(); + } + }; + AsyncLocalStorage.prototype.exit = function (callback, ...args) { + return this.run(undefined, callback, ...args); + }; + AsyncLocalStorage.prototype.getStore = function () { return this._entered ? this._store : undefined; }; + AsyncLocalStorage.prototype.enterWith = function (store) { this._store = store; this._entered = true; }; + AsyncLocalStorage.prototype.disable = function () { this._store = undefined; this._entered = false; }; + function AsyncResource(type) { this.type = type; } + AsyncResource.prototype.runInAsyncScope = function (fn, thisArg, ...args) { return fn.apply(thisArg, args); }; + AsyncResource.prototype.emitDestroy = function () { return this; }; + AsyncResource.prototype.asyncId = function () { return 1; }; + AsyncResource.prototype.triggerAsyncId = function () { return 0; }; + AsyncResource.bind = (fn) => fn; + const asyncHooks = { + AsyncLocalStorage, AsyncResource, + executionAsyncId: () => 1, + triggerAsyncId: () => 0, + executionAsyncResource: () => ({}), + createHook: () => ({ enable() { return this; }, disable() { return this; } }), + }; + globalThis.__sxnAsyncHooks = asyncHooks; + + // ---------------- node:inspector ---------------- + // There is no debug protocol behind this. It exists so that a library can + // ask whether a session is open and get "no" instead of a crash. + const inspector = { + url: () => undefined, + open() { throw new Error("inspector.open is not supported: this runtime has no debug protocol"); }, + close() {}, + waitForDebugger() { throw new Error("inspector.waitForDebugger is not supported"); }, + console: globalThis.console, + Session: function Session() { throw new Error("inspector.Session is not supported: this runtime has no debug protocol"); }, + }; + inspector.promises = { Session: inspector.Session }; + globalThis.__sxnInspector = inspector; + + // ---------------- node:dgram ---------------- + // A real UDP socket (uv_udp_t, in src/network.c) with the EventEmitter + // shape Node gives it. Multicast is not wired up. + function Socket(options) { + EE.call(this); + this.type = (options && (options.type || options)) === "udp6" ? "udp6" : "udp4"; + this._port = 0; + this._handle = __sxnUdpOpen(this.type === "udp6", (bytes, address, port) => { + this.emit("message", Buffer.from(bytes), { address, port, family: this.type === "udp6" ? "IPv6" : "IPv4", size: bytes.length }); + }); + } + inheritEE(Socket); + Socket.prototype.bind = function (port, address, cb) { + if (typeof port === "object" && port !== null) { address = port.address; port = port.port; } + if (typeof address === "function") { cb = address; address = undefined; } + this._port = __sxnUdpBind(this._handle, Number(port) || 0, address); + if (cb) this.once("listening", cb); + process.nextTick(() => this.emit("listening")); + return this; + }; + Socket.prototype.send = function (data, port, address, cb) { + if (typeof address === "function") { cb = address; address = undefined; } + const bytes = typeof data === "string" ? Buffer.from(data, "utf8") + : ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + : new Uint8Array(data); + let error = null, sent = 0; + try { sent = __sxnUdpSend(this._handle, bytes, Number(port) || 0, address); } + catch (e) { error = e; } + if (cb) process.nextTick(() => cb(error, sent)); + else if (error) process.nextTick(() => this.emit("error", error)); + return this; + }; + Socket.prototype.address = function () { + return { address: this.type === "udp6" ? "::" : "0.0.0.0", port: this._port, family: this.type === "udp6" ? "IPv6" : "IPv4" }; + }; + Socket.prototype.close = function (cb) { + __sxnUdpClose(this._handle); + if (cb) this.once("close", cb); + process.nextTick(() => this.emit("close")); + return this; + }; + Socket.prototype.ref = function () { return this; }; + Socket.prototype.unref = function () { return this; }; + Socket.prototype.setBroadcast = function () { return this; }; + const dgram = { + Socket, + createSocket(options, listener) { + const s = new Socket(options); + if (typeof options === "object" && options && typeof listener !== "function") listener = options.listener; + if (typeof listener === "function") s.on("message", listener); + return s; + }, + }; + globalThis.__sxnDgram = dgram; + + // ---------------- node:console / node:constants ---------------- + // Both are the older shape of things that live elsewhere now: the global + // console, and the constants that hang off fs, os and crypto. + globalThis.__sxnConsole = Object.assign(Object.create(null), globalThis.console, { + Console: function Console() { return globalThis.console; }, + }); + globalThis.__sxnConstants = Object.assign({}, fs.constants, os.constants, { + SIGINT: 2, SIGTERM: 15, SIGKILL: 9, + }); + + // ---------------- node:punycode ---------------- + // The algorithm from RFC 3492, which is small enough to be worth having + // rather than a stub: `url` used to need it and some packages still do. + const punyBase = 36, punyTMin = 1, punyTMax = 26, punySkew = 38, + punyDamp = 700, punyInitialBias = 72, punyInitialN = 128; + function punyAdapt(delta, numPoints, firstTime) { + delta = firstTime ? Math.floor(delta / punyDamp) : delta >> 1; + delta += Math.floor(delta / numPoints); + let k = 0; + while (delta > ((punyBase - punyTMin) * punyTMax) >> 1) { + delta = Math.floor(delta / (punyBase - punyTMin)); + k += punyBase; + } + return k + Math.floor(((punyBase - punyTMin + 1) * delta) / (delta + punySkew)); + } + function punyDecode(input) { + const output = []; + const basic = input.lastIndexOf("-"); + let n = punyInitialN, bias = punyInitialBias, i = 0; + for (let j = 0; j < (basic < 0 ? 0 : basic); j++) output.push(input.charCodeAt(j)); + for (let index = basic < 0 ? 0 : basic + 1; index < input.length;) { + const oldi = i; + for (let w = 1, k = punyBase;; k += punyBase) { + const code = input.charCodeAt(index++); + const digit = code - 48 < 10 ? code - 22 : code - 65 < 26 ? code - 65 : code - 97 < 26 ? code - 97 : punyBase; + if (digit >= punyBase) throw new RangeError("Invalid input"); + i += digit * w; + const t = k <= bias ? punyTMin : k >= bias + punyTMax ? punyTMax : k - bias; + if (digit < t) break; + w *= punyBase - t; + } + bias = punyAdapt(i - oldi, output.length + 1, oldi === 0); + n += Math.floor(i / (output.length + 1)); + i %= output.length + 1; + output.splice(i++, 0, n); + } + return String.fromCodePoint(...output); + } + function punyEncode(input) { + const points = Array.from(input).map((c) => c.codePointAt(0)); + const basic = points.filter((c) => c < 128); + const output = basic.map((c) => String.fromCharCode(c)); + let handled = basic.length; + if (handled) output.push("-"); + let n = punyInitialN, delta = 0, bias = punyInitialBias; + while (handled < points.length) { + let m = Infinity; + for (const c of points) if (c >= n && c < m) m = c; + delta += (m - n) * (handled + 1); + n = m; + for (const c of points) { + if (c < n) delta++; + else if (c === n) { + let q = delta; + for (let k = punyBase;; k += punyBase) { + const t = k <= bias ? punyTMin : k >= bias + punyTMax ? punyTMax : k - bias; + if (q < t) break; + output.push(String.fromCharCode(punyDigit(t + ((q - t) % (punyBase - t))))); + q = Math.floor((q - t) / (punyBase - t)); + } + output.push(String.fromCharCode(punyDigit(q))); + bias = punyAdapt(delta, handled + 1, handled === basic.length); + delta = 0; + handled++; + } + } + delta++; + n++; + } + return output.join(""); + } + const punyDigit = (d) => d + 22 + (d < 26 ? 75 : 0); + const mapDomain = (text, fn) => text.split(".").map(fn).join("."); + const punycode = { + encode: punyEncode, + decode: punyDecode, + toASCII: (text) => mapDomain(text, (part) => + /[^\x00-\x7F]/.test(part) ? "xn--" + punyEncode(part) : part), + toUnicode: (text) => mapDomain(text, (part) => + part.startsWith("xn--") ? punyDecode(part.slice(4)) : part), + ucs2: { + decode: (text) => Array.from(text).map((c) => c.codePointAt(0)), + encode: (points) => String.fromCodePoint(...points), + }, + version: "2.3.1", + }; + globalThis.__sxnPunycode = punycode; + + // ---------------- node:diagnostics_channel ---------------- + // Named channels with subscribers, which is all of it that does not depend + // on async context tracking. + const channels = new Map(); + function Channel(name) { this.name = name; this._subscribers = []; } + Object.defineProperty(Channel.prototype, "hasSubscribers", { + get() { return this._subscribers.length > 0; }, + }); + Channel.prototype.publish = function (message) { + for (const fn of this._subscribers.slice()) { + try { fn(message, this.name); } catch { /* a subscriber must not break the publisher */ } + } + }; + Channel.prototype.subscribe = function (fn) { this._subscribers.push(fn); }; + Channel.prototype.unsubscribe = function (fn) { + const i = this._subscribers.indexOf(fn); + if (i < 0) return false; + this._subscribers.splice(i, 1); + return true; + }; + Channel.prototype.bindStore = function () {}; + Channel.prototype.runStores = function (message, fn, thisArg, ...args) { + this.publish(message); + return fn.apply(thisArg, args); + }; + function channelFor(name) { + let c = channels.get(name); + if (!c) { c = new Channel(name); channels.set(name, c); } + return c; + } + const diagnosticsChannel = { + Channel, + channel: channelFor, + hasSubscribers: (name) => channels.has(name) && channels.get(name).hasSubscribers, + subscribe: (name, fn) => channelFor(name).subscribe(fn), + unsubscribe: (name, fn) => channelFor(name).unsubscribe(fn), + tracingChannel(name) { + return { + start: channelFor(name + ":start"), end: channelFor(name + ":end"), + asyncStart: channelFor(name + ":asyncStart"), asyncEnd: channelFor(name + ":asyncEnd"), + error: channelFor(name + ":error"), + traceSync(fn, context, thisArg, ...args) { return fn.apply(thisArg, args); }, + tracePromise(fn, context, thisArg, ...args) { return fn.apply(thisArg, args); }, + traceCallback(fn, position, context, thisArg, ...args) { return fn.apply(thisArg, args); }, + }; + }, + }; + globalThis.__sxnDiagnosticsChannel = diagnosticsChannel; + }; + // The names above, each a getter that builds the whole group once and then + // gets out of the way -- the group is one closure, so splitting it further + // would buy nothing. + let builtLater = false; + for (const name of ["__sxnChildProcess", "__sxnDns", "__sxnDnsPromises", "__sxnHttps", "__sxnTls", "__sxnHttp2", "__sxnStreamWeb", "__sxnVm", "__sxnV8", "__sxnWorkerThreads", "__sxnCluster", "__sxnReadline", "__sxnReadlinePromises", "__sxnAsyncHooks", "__sxnInspector", "__sxnDgram", "__sxnConsole", "__sxnConstants", "__sxnPunycode", "__sxnDiagnosticsChannel"]) { + Object.defineProperty(globalThis, name, { + configurable: true, + get() { + if (!builtLater) { + builtLater = true; + for (const other of ["__sxnChildProcess", "__sxnDns", "__sxnDnsPromises", "__sxnHttps", "__sxnTls", "__sxnHttp2", "__sxnStreamWeb", "__sxnVm", "__sxnV8", "__sxnWorkerThreads", "__sxnCluster", "__sxnReadline", "__sxnReadlinePromises", "__sxnAsyncHooks", "__sxnInspector", "__sxnDgram", "__sxnConsole", "__sxnConstants", "__sxnPunycode", "__sxnDiagnosticsChannel"]) delete globalThis[other]; + laterBuiltins(); + } + return globalThis[name]; + }, + set(value) { + Object.defineProperty(globalThis, name, { value, writable: true, configurable: true }); + }, + }); + } + })(); diff --git a/src/package.c b/src/package.c index 6012fbe..0be825a 100644 --- a/src/package.c +++ b/src/package.c @@ -79,7 +79,7 @@ static int write_initial_manifest(void) { if (check) { fclose(check); fputs("sxn init: package.json already exists\n", stderr); return 1; } FILE *file = fopen("package.json", "wb"); if (!file) { perror("package.json"); return 1; } - const char *manifest = "{\n \"name\": \"sxfe-app\",\n \"version\": \"0.0.1\",\n \"type\": \"module\",\n \"scripts\": {\n \"start\": \"sxn index.sx\"\n },\n \"trustedDependencies\": []\n}\n"; + const char *manifest = "{\n \"name\": \"sxfe-app\",\n \"version\": \"0.0.2\",\n \"type\": \"module\",\n \"scripts\": {\n \"start\": \"sxn index.sx\"\n },\n \"trustedDependencies\": []\n}\n"; fwrite(manifest, 1, strlen(manifest), file); fclose(file); puts("Created package.json"); return 0; } @@ -195,7 +195,7 @@ static int curl_get(const char *url, MemBuf *out) { if (!easy) return -1; curl_easy_setopt(easy, CURLOPT_URL, url); curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(easy, CURLOPT_USERAGENT, "sxn/0.0.1"); + curl_easy_setopt(easy, CURLOPT_USERAGENT, "sxn/0.0.2"); curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, membuf_write); curl_easy_setopt(easy, CURLOPT_WRITEDATA, out); CURLcode rc = curl_easy_perform(easy); diff --git a/tests/fixtures/emit_fusion.mjs b/tests/fixtures/emit_fusion.mjs index d562098..8c5470b 100644 --- a/tests/fixtures/emit_fusion.mjs +++ b/tests/fixtures/emit_fusion.mjs @@ -14,6 +14,10 @@ const WANT = { "once": "1", "listener this": "true", "remove during emit": "[\"f1\",\"f2\",\"f1\"]", +"off removes one of two": "2", +"off removes the second": "1", +"off keeps the order": "[\"a\",\"c\"]", +"remove a duplicate during emit": "[\"first\",\"dup\",\"dup\",\"first\",\"dup\"]", "throws": "\"RangeError:boom\"", "unhandled error": "\"TypeError\"", "off/on": "2", @@ -60,6 +64,21 @@ const p = (n, v) => { { const e=new EventEmitter(); const o=[]; const f2=()=>o.push("f2"); const f1=()=>{o.push("f1"); e.off("x",f2);}; e.on("x",f1); e.on("x",f2); e.emit("x"); e.emit("x"); p("remove during emit",o); } +// off() removes one instance of a listener registered twice, and leaves the +// order of the rest alone. Outside an emit it closes the list up in place; +// inside one it has to copy, which these two cases pin. +{ const e=new EventEmitter(); const f=()=>{}; + e.on("x",f); e.on("x",f); e.on("x",()=>{}); e.off("x",f); + p("off removes one of two", e.listenerCount("x")); + e.off("x",f); p("off removes the second", e.listenerCount("x")); } +{ const e=new EventEmitter(); const o=[]; + const a=()=>o.push("a"), b=()=>o.push("b"), c=()=>o.push("c"); + e.on("y",a); e.on("y",b); e.on("y",c); e.off("y",b); e.emit("y"); + p("off keeps the order", o); } +{ const e=new EventEmitter(); const o=[]; const dup=()=>o.push("dup"); + const first=()=>{o.push("first"); e.off("x",dup);}; + e.on("x",first); e.on("x",dup); e.on("x",dup); + e.emit("x"); e.emit("x"); p("remove a duplicate during emit",o); } // throwing listener propagates { const e=new EventEmitter(); e.on("x",()=>{throw new RangeError("boom");}); p("throws",(()=>{try{e.emit("x",1);return "no"}catch(err){return err.constructor.name+":"+err.message}})()); } diff --git a/tests/fixtures/events_maxlisteners.mjs b/tests/fixtures/events_maxlisteners.mjs new file mode 100644 index 0000000..f86f71a --- /dev/null +++ b/tests/fixtures/events_maxlisteners.mjs @@ -0,0 +1,94 @@ +// Listeners added and never removed are the ordinary way a long-running +// server grows without bound, and the count crossing a limit is the standard +// signal. EventEmitter.defaultMaxListeners existed here as a constant nothing +// read; this asserts it now does something, once per emitter and event, and +// that a program which legitimately wants more can say so. +// +// The capture point is process.emitWarning, which is where both this runtime +// and Node route the warning, so this fixture's expectations can be checked +// against Node by running it there. + +import { EventEmitter } from "node:events"; + +let failures = 0; +const check = (name, got, want) => { + if (got !== want) { + console.log("FAIL", name, "got", JSON.stringify(got), "want", JSON.stringify(want)); + failures += 1; + } +}; + +const warnings = []; +const realEmitWarning = process.emitWarning; +process.emitWarning = (w) => { + const name = w && w.name && w.name !== "Error" ? w.name + ": " : ""; + warnings.push(name + (w && w.message ? w.message : String(w))); +}; +const since = () => warnings.length; +const added = (n) => warnings.slice(n); + +check("default is 10", EventEmitter.defaultMaxListeners, 10); + +// Ten is fine; the eleventh is what warns. +const a = new EventEmitter(); +let mark = since(); +for (let i = 0; i < 10; i++) a.on("x", () => {}); +check("ten listeners are silent", added(mark).length, 0); + +mark = since(); +a.on("x", () => {}); +const first = added(mark); +check("the eleventh warns", first.length, 1); +check("names the count", first[0].includes("11 x listeners added"), true); +check("names the limit", first[0].includes("MaxListeners is 10"), true); +check("is a MaxListenersExceededWarning", first[0].includes("MaxListenersExceededWarning"), true); + +// Once per emitter and event, not once per registration. +mark = since(); +for (let i = 0; i < 20; i++) a.on("x", () => {}); +check("warns once, not per listener", added(mark).length, 0); + +// A different event on the same emitter warns on its own. +mark = since(); +for (let i = 0; i < 11; i++) a.on("y", () => {}); +check("a second event warns separately", added(mark).length, 1); + +// Nothing about the listener store changed. +check("listeners are all still there", a.listenerCount("x"), 31); +check("and still callable", (() => { let n = 0; a.on("z", () => n++); a.emit("z"); return n; })(), 1); + +// Raising the limit suppresses it. +const b = new EventEmitter(); +b.setMaxListeners(50); +check("getMaxListeners reflects it", b.getMaxListeners(), 50); +mark = since(); +for (let i = 0; i < 40; i++) b.on("x", () => {}); +check("under a raised limit is silent", added(mark).length, 0); +check("and the listeners are kept", b.listenerCount("x"), 40); + +// Zero means unlimited, as in Node. +const c = new EventEmitter(); +c.setMaxListeners(0); +mark = since(); +for (let i = 0; i < 100; i++) c.on("x", () => {}); +check("zero means unlimited", added(mark).length, 0); + +// The default is a real accessor over the value the check uses. +EventEmitter.defaultMaxListeners = 2; +check("the default is settable", EventEmitter.defaultMaxListeners, 2); +const d = new EventEmitter(); +mark = since(); +d.on("q", () => {}); d.on("q", () => {}); d.on("q", () => {}); +const lowered = added(mark); +check("a lowered default warns", lowered.length, 1); +check("at the lowered limit", lowered[0].includes("MaxListeners is 2"), true); +EventEmitter.defaultMaxListeners = 10; + +// An emitter that never crosses the limit never allocates the warn marker. +const e = new EventEmitter(); +e.on("x", () => {}); +check("no marker until it warns", e._warnedEvents, undefined); + +process.emitWarning = realEmitWarning; +if (failures !== 0) throw new Error(failures + " maxListeners checks failed"); +console.log("maxListeners: all checks passed"); diff --git a/tests/fixtures/json_edges.mjs b/tests/fixtures/json_edges.mjs new file mode 100644 index 0000000..f5ad4bf --- /dev/null +++ b/tests/fixtures/json_edges.mjs @@ -0,0 +1,142 @@ +// JSON.parse and JSON.stringify around the edges of the fast paths added for +// speed: an escape-free run ends exactly at a quote, a backslash, a control +// character or a non-ASCII byte, and the scan works a word at a time, so the +// interesting cases sit at those boundaries. +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok?"ok ":"FAIL ") + n + " got=" + JSON.stringify(got) + " want=" + JSON.stringify(want)); }; + +const cases = [ + ["plain", "hello world"], + ["empty", ""], + ["quote", 'a"b'], + ["backslash", "a\\b"], + ["newline", "a\nb"], + ["tab", "a\tb"], + ["control", "ab"], + ["del", "ab"], + ["latin1", "café"], + ["cjk", "日本語"], + ["emoji", "🎉 done"], + ["lone high surrogate", "a\ud800b"], + ["lone low surrogate", "a\udc00b"], + ["pair", "😀"], + ["long ascii", "x".repeat(1000)], + ["escape at 7", '1234567"tail'], + ["escape at 8", '12345678"tail'], + ["escape at 9", '123456789"tail'], + ["non-ascii at 8", "12345678é9"], + ["control at 8", "123456789"], + ["backslash at 8", "12345678\\9"], +]; +for (const [name, value] of cases) { + check(`round trip ${name}`, JSON.parse(JSON.stringify(value)), value); +} + +check("quote escape", JSON.stringify('a"b'), '"a\\"b"'); +check("backslash escape", JSON.stringify("a\\b"), '"a\\\\b"'); +check("control escape", JSON.stringify(""), '"\\u0001"'); +check("lone surrogate escape", JSON.stringify("\ud800"), '"\\ud800"'); +check("emoji stays whole", JSON.stringify("🎉"), '"🎉"'); +check("tab escape", JSON.stringify("\t"), '"\\t"'); +check("del is not escaped", JSON.stringify(""), '""'); + +check("parse \\u", JSON.parse('"\\u0041\\u00e9"'), "Aé"); +check("parse escapes", JSON.parse('"a\\nb\\tc\\\\d\\"e\\/f"'), 'a\nb\tc\\d"e/f'); +check("parse utf8", JSON.parse('"日本語"'), "日本語"); + +// Numbers: the integer fast path has to agree with strtod exactly. +const numbers = ["0", "-0", "1", "-1", "42", "2147483647", "-2147483648", + "2147483648", "-2147483649", "999999999999999999", + "1000000000000000000", "9007199254740993", + "1.5", "-2.25", "1e3", "1E-3", "1.7976931348623157e308", + "5e-324", "0.1", "123456789012345678901234567890"]; +for (const n of numbers) check(`number ${n}`, JSON.parse(n), Number(n)); +check("negative zero keeps its sign", 1 / JSON.parse("-0"), -Infinity); + +const doc = JSON.parse('{"a":[1,2,{"b":"c"}],"d":null,"e":true}'); +check("nested", JSON.stringify(doc), '{"a":[1,2,{"b":"c"}],"d":null,"e":true}'); +check("key with escape", JSON.stringify({ 'a"b': 1 }), '{"a\\"b":1}'); +check("reviver", JSON.parse('{"n":2}', (k, v) => typeof v === "number" ? v * 3 : v).n, 6); +check("replacer", JSON.stringify({ a: 1, b: 2 }, ["a"]), '{"a":1}'); +check("indent", JSON.stringify({ a: 1 }, null, 2), '{\n "a": 1\n}'); +check("toJSON gets the key", JSON.stringify({ k: { toJSON: (key) => key } }), '{"k":"k"}'); + +// stringify caches what it learns about an object's shape; anything that +// changes where a property lives has to invalidate that. +check("toJSON on a shape seen before", JSON.stringify([{ a: 1 }, { a: 1, toJSON: () => "x" }]), '[{"a":1},"x"]'); +{ + // A getter installs toJSON on Object.prototype after an object of the same + // shape has already been written; the ones after it must pick it up. + const doc = { + first: { a: 1 }, + hook: { get a() { Object.prototype.toJSON = function () { return "late"; }; return 0; } }, + second: { a: 2 }, + }; + const out = JSON.stringify(doc); + delete Object.prototype.toJSON; + check("toJSON installed mid-run", out, '{"first":{"a":1},"hook":{"a":0},"second":"late"}'); +} +{ + const seen = { a: { x: 1 }, b: { x: 2 } }; + Object.prototype.toJSON = function () { return "P"; }; + const out = JSON.stringify(seen); + delete Object.prototype.toJSON; + check("toJSON from the prototype", out, '"P"'); +} +{ + // Two proxies with different traps but the same (empty) shape: what the + // first one answers about toJSON must not be assumed of the second. + const p1 = new Proxy({ a: 1 }, { get(t, k) { return k === "toJSON" ? undefined : t[k]; } }); + const p2 = new Proxy({ a: 1 }, { get(t, k) { return k === "toJSON" ? () => "second" : t[k]; } }); + check("proxies are not one shape", JSON.stringify([p1, p2]), '[{"a":1},"second"]'); +} +{ + // Two objects of the same shape, one of which has a toJSON value stored + // into the slot the other left undefined. A shape says which properties + // exist, not what they hold. + const make = () => ({ x: 1, toJSON: undefined }); + const a = make(), b = make(); + b.toJSON = () => "hijacked"; + check("same shape, different toJSON", JSON.stringify([a, b]), '[{"x":1},"hijacked"]'); +} +{ + // The same through a prototype: the value is replaced in place, which + // moves nothing. + const proto = { toJSON: undefined }; + const a = Object.create(proto), b = Object.create(proto); + a.x = 1; b.x = 2; + const first = JSON.stringify(a); + proto.toJSON = () => "from proto"; + check("a prototype's toJSON replaced in place", first + JSON.stringify(b), '{"x":1}"from proto"'); +} +// The key list is taken once, before any getter runs: a getter that changes +// another key's enumerability cannot change what is written. +check("enumerability is a snapshot", + JSON.stringify({ get a() { Object.defineProperty(this, "b", { value: 2, enumerable: false }); return 1; }, b: 2 }), + '{"a":1,"b":2}'); +check("a key made enumerable mid-run", + JSON.stringify(Object.defineProperties({}, { + a: { enumerable: true, get() { Object.defineProperty(this, "b", { enumerable: true }); return 1; } }, + b: { value: 2, enumerable: false, configurable: true }, + })), + '{"a":1}'); +check("__proto__ is an own key", JSON.stringify(JSON.parse('{"__proto__":{"x":1}}')), '{"__proto__":{"x":1}}'); +check("__proto__ does not set the prototype", Object.getPrototypeOf(JSON.parse('{"__proto__":{"x":1}}')) === Object.prototype, true); +check("non-enumerable keys are skipped", JSON.stringify(Object.defineProperty({ a: 1 }, "b", { value: 2 })), '{"a":1}'); +check("a getter's value is used", JSON.stringify({ get a() { return 7; } }), '{"a":7}'); +check("numeric keys sort first", JSON.stringify({ b: 1, 2: 2, a: 3, 1: 4 }), '{"1":4,"2":2,"b":1,"a":3}'); +check("symbol keys are skipped", JSON.stringify({ [Symbol("s")]: 1, a: 2 }), '{"a":2}'); +check("inherited keys are skipped", JSON.stringify(Object.create({ inherited: 1 }, { own: { value: 2, enumerable: true } })), '{"own":2}'); +check("a key deleted by a getter", JSON.stringify({ get a() { delete this.b; return 1; }, b: 2 }), '{"a":1}'); +check("array holes are null", JSON.stringify([1, , 3]), '[1,null,3]'); +check("Date uses its toJSON", JSON.stringify({ d: new Date(0) }), '{"d":"1970-01-01T00:00:00.000Z"}'); + +for (const text of ['{"a":}', '"unterminated', '"ab"', "[1,]", "01", "1.", "+1", "'x'"]) { + let threw = false; + try { JSON.parse(text); } catch { threw = true; } + check(`rejects ${JSON.stringify(text)}`, threw, true); +} + +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/json_fuzz.mjs b/tests/fixtures/json_fuzz.mjs new file mode 100644 index 0000000..dac656e --- /dev/null +++ b/tests/fixtures/json_fuzz.mjs @@ -0,0 +1,90 @@ +// A seeded random walk over JSON, to keep the fast paths in JSON.parse and +// JSON.stringify honest: documents are generated, written, parsed back and +// written again, and every intermediate string is hashed. Running this under +// Node gives the same hash, so a divergence anywhere shows up as one number. +// Deterministic: same seed, same documents, same hash. + +let state = 0x2f6e2b1; +const rand = () => { + // xorshift32, so Node and this runtime walk identical documents. + state ^= state << 13; state >>>= 0; + state ^= state >>> 17; + state ^= state << 5; state >>>= 0; + return state; +}; +const pick = (n) => rand() % n; + +const KEYS = ["id", "name", "ok", "value", "", "__proto__", "toJSON", "a b", 'q"q', "ключ", "🙂", + "aVeryLongPropertyNameThatGoesPastTheShortStringCases", "0", "1", "12", "-1", "1e3"]; +const STRINGS = ["", "plain", "with \"quote\"", "back\\slash", "tab\there", "new\nline", + "controlchar", "del", "café", "日本語", "🎉 emoji", "\ud800 lone high", + "\udc00 lone low", "x".repeat(200), "12345678\"boundary", "1234567é8"]; +const NUMBERS = [0, -0, 1, -1, 42, 2147483647, -2147483648, 2147483648, 9007199254740991, + 1e21, 1e-7, 0.1, -2.25, 1.7976931348623157e308, 5e-324, 123456789012345678901234567890]; + +function value(depth) { + switch (pick(depth > 3 ? 5 : 8)) { + case 0: return null; + case 1: return pick(2) === 0; + case 2: return NUMBERS[pick(NUMBERS.length)]; + case 3: return STRINGS[pick(STRINGS.length)]; + case 4: return rand() / 1000; + case 5: { + const n = pick(6); + const out = []; + for (let i = 0; i < n; i++) out.push(value(depth + 1)); + return out; + } + default: { + const n = pick(6); + const out = {}; + for (let i = 0; i < n; i++) out[KEYS[pick(KEYS.length)]] = value(depth + 1); + return out; + } + } +} + +// FNV-1a over every string produced, so one number covers every document. +let hash = 0x811c9dc5; +const feed = (s) => { + for (let i = 0; i < s.length; i++) { + hash ^= s.charCodeAt(i) & 0xff; + hash = Math.imul(hash, 0x01000193) >>> 0; + hash ^= s.charCodeAt(i) >>> 8; + hash = Math.imul(hash, 0x01000193) >>> 0; + } +}; + +let documents = 0; +for (let i = 0; i < 4000; i++) { + const v = value(0); + const text = JSON.stringify(v); + if (text === undefined) continue; + feed(text); + const back = JSON.parse(text); + const again = JSON.stringify(back); + feed(again); + if (again !== text) { + console.log("FAIL round trip differs at document", i); + console.log(" first :", text.slice(0, 200)); + console.log(" second:", again.slice(0, 200)); + process.exit(1); + } + // The same document with an indent, which takes the other separator path. + feed(JSON.stringify(v, null, 2)); + // And through a reviver and a replacer, which take the general paths. + feed(JSON.stringify(JSON.parse(text, (k, x) => x))); + feed(JSON.stringify(v, (k, x) => x)); + documents++; +} + +// Node's hash for this seed. It is the whole point of the file: if anything +// about parsing or writing changes by one byte, this stops matching. +const EXPECTED = "edf3782d"; +console.log("documents:", documents); +console.log("hash:", hash.toString(16)); +if (hash.toString(16) !== EXPECTED) { + console.log("FAIL hash differs from Node's", hash.toString(16), "want", EXPECTED); + process.exit(1); +} +console.log("ALL PASS"); diff --git a/tests/fixtures/leak_cycle_stress.sx b/tests/fixtures/leak_cycle_stress.sx new file mode 100644 index 0000000..c1ab408 --- /dev/null +++ b/tests/fixtures/leak_cycle_stress.sx @@ -0,0 +1,76 @@ +// Cyclic garbage must be reclaimable on demand. +// +// Reference counting frees an object when its last reference goes, which is +// never for a cycle: a closure that captures the object holding it keeps both +// alive with nobody able to reach either. Only the cycle collector can free +// them, and it runs only when an allocation crosses a threshold -- so a +// process that stops allocating stops collecting. Sxn.gc() is the explicit +// way to ask, and this asserts it actually works. +// +// Assertions are on `mallocSize`, the engine allocator's own accounting, not +// on `rss`. The system allocator is free to keep freed pages rather than +// return them to the kernel, so RSS routinely stays high after a sweep that +// reclaimed everything; asserting on it would fail for a reason that is not +// a leak. RSS is reported by benchmarks/engine/gc_idle_probe.sx instead. + +const CYCLES = 100000; + +let failures = 0; +function check(name: string, ok: bool, detail: string): void { + if (!ok) { console.log("FAIL", name, detail); failures += 1; } +} + +const before = Sxn.memoryUsage().mallocSize; + +// A mock request loop. Each "request" builds the two shapes that defeat +// refcounting: an object that points at itself, and a closure pair where the +// function reaches an object that holds the function. +// +// The batch is held while it is built and dropped at the end, which is the +// shape that actually leaks. A loop that drops each item as it goes does not: +// it keeps allocating, so the allocation trigger keeps firing and collects +// the cycles behind it. What breaks is a burst held live -- the collection at +// its peak sizes the next threshold for that peak -- and then released, after +// which nothing allocates enough to cross the raised bar again. +function handleBatch(n: i32) { + const batch = []; + for (let i = 0; i < n; i++) { + const state = { id: i, body: new Array(24).fill(i) }; + state.self = state; + const respond = () => state.body.length; + state.respond = respond; + batch.push(respond); + } + return batch.length; +} + +handleBatch(CYCLES); // built, then dropped on return + +const peak = Sxn.memoryUsage().mallocSize; +check("the cycles survived the drop", peak > before + 8 * 1024 * 1024, + "peak " + peak + " vs before " + before); + +const after = Sxn.gc(); + +// Back to within 1 MB of where we started. The margin covers the fixture's +// own strings and the console output, not the 100k cycles. +check("Sxn.gc reclaims the cycles", after < before + 1024 * 1024, + "after " + after + " vs before " + before); +check("Sxn.gc reports what it left", after === Sxn.memoryUsage().mallocSize, + "returned " + after); + +// Sweeping again must be stable rather than reclaiming more, which would mean +// the first pass had missed something reachable only after it ran. +const twice = Sxn.gc(); +check("a second sweep is a no-op", Math.abs(twice - after) < 64 * 1024, + "first " + after + " second " + twice); + +// The collector must not have taken anything still reachable with it. +const live = []; +for (let i = 0; i < 1000; i++) { const s = { i }; s.self = s; live.push(s); } +Sxn.gc(); +check("live cycles survive a sweep", live.length === 1000 && live[999].self === live[999], + "live " + live.length); + +if (failures !== 0) throw new Error(failures + " cycle-stress checks failed"); +console.log("cycle stress: reclaimed " + (peak - after) + " bytes, all checks passed"); diff --git a/tests/fixtures/leak_idle_sweep.mjs b/tests/fixtures/leak_idle_sweep.mjs new file mode 100644 index 0000000..e7fd133 --- /dev/null +++ b/tests/fixtures/leak_idle_sweep.mjs @@ -0,0 +1,62 @@ +// The cycle sweep must fire on its own when the event loop goes quiet. +// +// leak_cycle_stress.sx covers Sxn.gc(), which any program can call. This +// covers the case nobody calls anything: a daemon takes a burst, the burst +// leaves cyclic garbage, and then it waits for the next request. Without the +// sweep that garbage is held forever, because QuickJS collects only from an +// allocation and a waiting process makes none -- and the collection at the +// burst's peak has already raised the threshold above what was leaked. +// +// Run with --no-idle-gc this fixture is expected to report NOT-SWEPT; ctest +// runs it both ways. + +let failures = 0; +const check = (name, ok, detail) => { + if (!ok) { console.log("FAIL", name, detail); failures += 1; } +}; + +const sweepDisabled = process.argv.includes("--expect-no-sweep"); + +function burst(n) { + const batch = []; + for (let i = 0; i < n; i++) { + const s = { i, body: new Array(24).fill(i) }; + s.self = s; + s.cb = () => s.body; + batch.push(s); + } + return batch.length; +} + +const before = Sxn.memoryUsage().mallocSize; +burst(200000); +const peak = Sxn.memoryUsage(); +check("burst crossed the 32MB floor", peak.mallocSize > before + 32 * 1024 * 1024, + "peak " + peak.mallocSize); + +// Idle. Each tick blocks the loop for longer than the sweep's threshold, so +// the loop can tell it is waiting rather than working. Nothing here calls +// Sxn.gc(). +let ticks = 0; +await new Promise((done) => { + const id = setInterval(() => { + if (++ticks >= 4) { clearInterval(id); done(); } + }, 120); +}); + +const after = Sxn.memoryUsage(); +const swept = after.mallocSize < before + 4 * 1024 * 1024; + +if (sweepDisabled) { + check("--no-idle-gc holds the garbage", !swept, + "malloc " + after.mallocSize + " gc " + after.gcCount); + console.log(failures === 0 ? "idle sweep: NOT-SWEPT as expected" : "idle sweep: unexpected sweep"); +} else { + check("the loop swept while idle", swept, + "malloc " + after.mallocSize + " vs before " + before); + check("a collection is what did it", after.gcCount > peak.gcCount, + "gc " + peak.gcCount + " -> " + after.gcCount); + console.log("idle sweep: reclaimed " + (peak.mallocSize - after.mallocSize) + " bytes with no explicit call"); +} + +if (failures !== 0) throw new Error(failures + " idle-sweep checks failed"); diff --git a/tests/fixtures/node_assert.expected b/tests/fixtures/node_assert.expected new file mode 100644 index 0000000..9d02d20 --- /dev/null +++ b/tests/fixtures/node_assert.expected @@ -0,0 +1,130 @@ +strict numbers -> ok +loose numbers -> ok +isDeep numbers -> true +strict number vs string -> throws +loose number vs string -> ok +isDeep number vs string -> false +strict zero signs -> throws +loose zero signs -> ok +isDeep zero signs -> false +strict NaN -> ok +loose NaN -> ok +isDeep NaN -> true +strict null vs undefined -> throws +loose null vs undefined -> ok +isDeep null vs undefined -> false +strict empty objects -> ok +loose empty objects -> ok +isDeep empty objects -> true +strict flat objects -> ok +loose flat objects -> ok +isDeep flat objects -> true +strict key order -> ok +loose key order -> ok +isDeep key order -> true +strict extra key -> throws +loose extra key -> throws +isDeep extra key -> false +strict nested -> ok +loose nested -> ok +isDeep nested -> true +strict nested differs -> throws +loose nested differs -> throws +isDeep nested differs -> false +strict array vs object -> throws +loose array vs object -> throws +isDeep array vs object -> false +strict array holes -> throws +loose array holes -> throws +isDeep array holes -> false +strict dates -> ok +loose dates -> ok +isDeep dates -> true +strict dates differ -> throws +loose dates differ -> throws +isDeep dates differ -> false +strict invalid dates -> ok +loose invalid dates -> ok +isDeep invalid dates -> true +strict regexps -> ok +loose regexps -> ok +isDeep regexps -> true +strict regexps differ -> throws +loose regexps differ -> throws +isDeep regexps differ -> false +strict typed arrays -> ok +loose typed arrays -> ok +isDeep typed arrays -> true +strict typed arrays differ -> throws +loose typed arrays differ -> throws +isDeep typed arrays differ -> false +strict typed array kinds -> throws +loose typed array kinds -> throws +isDeep typed array kinds -> false +strict maps -> ok +loose maps -> ok +isDeep maps -> true +strict maps differ -> throws +loose maps differ -> throws +isDeep maps differ -> false +strict maps nested values -> ok +loose maps nested values -> ok +isDeep maps nested values -> true +strict sets -> ok +loose sets -> ok +isDeep sets -> true +strict sets differ -> throws +loose sets differ -> throws +isDeep sets differ -> false +strict prototypes -> throws +loose prototypes -> ok +isDeep prototypes -> false +strict classes -> ok +loose classes -> ok +isDeep classes -> true +strict different classes -> throws +loose different classes -> ok +isDeep different classes -> false +strict errors -> ok +loose errors -> ok +isDeep errors -> true +strict symbol keys ignored -> throws +loose symbol keys ignored -> ok +isDeep symbol keys ignored -> false +strict non-enumerable ignored -> ok +loose non-enumerable ignored -> ok +isDeep non-enumerable ignored -> true +strict cycles -> ok +loose cycles -> ok +isDeep cycles -> true +strict cycles differ -> throws +loose cycles differ -> throws +isDeep cycles differ -> false +strict shared subtrees -> ok +loose shared subtrees -> ok +isDeep shared subtrees -> true +strict strings -> ok +loose strings -> ok +isDeep strings -> true +strict boxed vs primitive -> throws +loose boxed vs primitive -> throws +isDeep boxed vs primitive -> false +strict boxed -> ok +loose boxed -> ok +isDeep boxed -> true +strict booleans -> throws +loose booleans -> ok +isDeep booleans -> false +strict functions -> throws +loose functions -> throws +isDeep functions -> false +ok true -> ok +ok false -> throws +equal loose -> ok +strictEqual -> throws +notStrictEqual -> ok +notDeepStrictEqual -> ok +throws catches -> ok +throws misses -> throws +match -> ok +match fails -> throws diff --git a/tests/fixtures/node_assert.mjs b/tests/fixtures/node_assert.mjs new file mode 100644 index 0000000..fc5c4e7 --- /dev/null +++ b/tests/fixtures/node_assert.mjs @@ -0,0 +1,95 @@ +// node:assert's structural comparison, which is C now. Every case is one +// question Node already has an answer for, so the answers are diffed rather +// than asserted here. +import assert from "node:assert"; +import util from "node:util"; +import { readFileSync } from "node:fs"; + +// Output is collected and matched against what Node printed, line for line. +const lines = []; +const console = { log: (...a) => { lines.push(a.join(" ")); } }; + + +const show = (name, fn) => { + let out; + try { fn(); out = "ok"; } catch (e) { out = e.name === "AssertionError" ? "throws" : "error:" + e.name; } + console.log(name + " -> " + out); +}; +const deep = (name, a, b) => { + show("strict " + name, () => assert.deepStrictEqual(a, b)); + show("loose " + name, () => assert.deepEqual(a, b)); + console.log("isDeep " + name + " -> " + util.isDeepStrictEqual(a, b)); +}; + +deep("numbers", 1, 1); +deep("number vs string", 1, "1"); +deep("zero signs", 0, -0); +deep("NaN", NaN, NaN); +deep("null vs undefined", null, undefined); +deep("empty objects", {}, {}); +deep("flat objects", { a: 1, b: "x" }, { a: 1, b: "x" }); +deep("key order", { a: 1, b: 2 }, { b: 2, a: 1 }); +deep("extra key", { a: 1 }, { a: 1, b: 2 }); +deep("nested", { a: { b: [1, 2, { c: 3 }] } }, { a: { b: [1, 2, { c: 3 }] } }); +deep("nested differs", { a: { b: [1, 2, { c: 3 }] } }, { a: { b: [1, 2, { c: 4 }] } }); +deep("array vs object", [1, 2], { 0: 1, 1: 2 }); +deep("array holes", [1, , 3], [1, undefined, 3]); +deep("dates", new Date(1000), new Date(1000)); +deep("dates differ", new Date(1000), new Date(1001)); +deep("invalid dates", new Date(NaN), new Date(NaN)); +deep("regexps", /ab+/gi, /ab+/gi); +deep("regexps differ", /ab+/g, /ab+/i); +deep("typed arrays", new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])); +deep("typed arrays differ", new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4])); +deep("typed array kinds", new Uint8Array([1]), new Int8Array([1])); +deep("maps", new Map([["a", 1]]), new Map([["a", 1]])); +deep("maps differ", new Map([["a", 1]]), new Map([["a", 2]])); +deep("maps nested values", new Map([["a", { x: 1 }]]), new Map([["a", { x: 1 }]])); +deep("sets", new Set([1, 2]), new Set([2, 1])); +deep("sets differ", new Set([1, 2]), new Set([1, 3])); +deep("prototypes", Object.create(null), {}); +class A { constructor() { this.x = 1; } } +class B { constructor() { this.x = 1; } } +deep("classes", new A(), new A()); +deep("different classes", new A(), new B()); +deep("errors", new Error("x"), new Error("x")); +deep("symbol keys ignored", { [Symbol("s")]: 1 }, {}); +deep("non-enumerable ignored", Object.defineProperty({ a: 1 }, "h", { value: 2 }), { a: 1 }); +{ + const a = { name: "a" }; a.self = a; + const b = { name: "a" }; b.self = b; + deep("cycles", a, b); + const c = { name: "c" }; c.self = c; + deep("cycles differ", a, c); +} +{ + const shared = { x: 1 }; + deep("shared subtrees", { l: shared, r: shared }, { l: { x: 1 }, r: { x: 1 } }); +} +deep("strings", "abc", "abc"); +deep("boxed vs primitive", new String("a"), "a"); +deep("boxed", new Number(1), new Number(1)); +deep("booleans", true, 1); +deep("functions", function f() {}, function f() {}); + +show("ok true", () => assert.ok(1)); +show("ok false", () => assert.ok(0)); +show("equal loose", () => assert.equal(1, "1")); +show("strictEqual", () => assert.strictEqual(1, "1")); +show("notStrictEqual", () => assert.notStrictEqual(1, 2)); +show("notDeepStrictEqual", () => assert.notDeepStrictEqual({ a: 1 }, { a: 2 })); +show("throws catches", () => assert.throws(() => { throw new Error("x"); })); +show("throws misses", () => assert.throws(() => {})); +show("match", () => assert.match("hello", /ell/)); +show("match fails", () => assert.match("hello", /zzz/)); + +const expected = readFileSync(new URL("./node_assert.expected", import.meta.url).pathname, "utf8").trimEnd().split("\n"); +let bad = 0; +for (let i = 0; i < Math.max(lines.length, expected.length); i++) { + if (lines[i] === expected[i]) continue; + bad++; + globalThis.console.log("FAIL want " + (expected[i] ?? "(nothing)")); + globalThis.console.log(" got " + (lines[i] ?? "(nothing)")); +} +globalThis.console.log(bad === 0 ? `node:assert: ${lines.length} answers identical to Node` : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_buffer_numbers.expected b/tests/fixtures/node_buffer_numbers.expected new file mode 100644 index 0000000..b2cb046 --- /dev/null +++ b/tests/fixtures/node_buffer_numbers.expected @@ -0,0 +1,43 @@ +hex fffe34121234c01dfeff3fc000000000 +readUInt8 255 readInt8 -2 +u16le 4660 u16be 4660 +i32le -123456 floatBE 1.5 +double 3.141592653589793 182d4454fb210940 +bigu64 12345678901234567890 ab54a98ceb1f0ad2 +bigi64 -42 +copied 5 world +range -> RangeError +compare -1 true false +"abc" "abc" 0 true 0 +"abc" "abd" -1 false -1 +"abd" "abc" 1 false 1 +"ab" "abc" -1 false -1 +"abc" "ab" 1 false 1 +"" "" 0 true 0 +"" "a" -1 false -1 +"a" "" 1 false 1 +"ÿ" "\u0000" 1 false 1 +sorted a,ab,b +hex 123456563412000000000000 +uintBE 1193046 uintLE 1193046 +intBE -1000 intLE -1000 fffc1818fcff +w1 255 ff0000000000 +w2 255 00ff00000000 +w3 255 0000ff000000 +w4 255 000000ff0000 +w5 255 00000000ff00 +w6 255 0000000000ff +swap16 0201040306050807 +swap32 0403020108070605 +swap64 0807060504030201 +odd swap -> RangeError +width 7 -> RangeError +wrote 5 hello..... +offset 2 ...hi..... +truncated 4 hell +length cap 3 hel....... +multibyte 3 "hé" +encoding arg 3 hey..... +src after writing to the copy: 1 copy: 99 +uint8array after: 1 buffer: 42 +arraybuffer view shares: 7 diff --git a/tests/fixtures/node_buffer_numbers.mjs b/tests/fixtures/node_buffer_numbers.mjs new file mode 100644 index 0000000..9c9e134 --- /dev/null +++ b/tests/fixtures/node_buffer_numbers.mjs @@ -0,0 +1,94 @@ +// Buffer's numeric accessors, write, copy, compare and the swaps -- all +// native (js_buffer_* in src/node.c, sxn_bytes_compare in src/network.c). +// Most of these did not exist here before. The expected output is Node's. +import { readFileSync } from "node:fs"; + +const printed = []; +const console = { log: (...args) => printed.push(args.join(" ")) }; + +{ +const b = Buffer.alloc(16); +b.writeUInt8(0xff, 0); b.writeInt8(-2, 1); +b.writeUInt16LE(0x1234, 2); b.writeUInt16BE(0x1234, 4); +b.writeInt32LE(-123456, 6); b.writeFloatBE(1.5, 10); +console.log("hex", b.toString("hex")); +console.log("readUInt8", b.readUInt8(0), "readInt8", b.readInt8(1)); +console.log("u16le", b.readUInt16LE(2), "u16be", b.readUInt16BE(4)); +console.log("i32le", b.readInt32LE(6), "floatBE", b.readFloatBE(10)); +const d = Buffer.alloc(8); d.writeDoubleLE(Math.PI, 0); +console.log("double", d.readDoubleLE(0), d.toString("hex")); +const big = Buffer.alloc(8); big.writeBigUInt64BE(12345678901234567890n, 0); +console.log("bigu64", String(big.readBigUInt64BE(0)), big.toString("hex")); +const sig = Buffer.alloc(8); sig.writeBigInt64LE(-42n, 0); +console.log("bigi64", String(sig.readBigInt64LE(0))); +const src = Buffer.from("hello world"), dst = Buffer.alloc(5); +console.log("copied", src.copy(dst, 0, 6, 11), dst.toString()); +try { b.readUInt32BE(14); } catch (e) { console.log("range ->", e.constructor.name); } +console.log("compare", Buffer.compare(Buffer.from("a"), Buffer.from("b")), Buffer.isEncoding("hex"), Buffer.isEncoding("nope")); +} + +{ +const cases = [["abc","abc"],["abc","abd"],["abd","abc"],["ab","abc"],["abc","ab"],["",""],["","a"],["a",""],["\xff","\x00"]]; +for (const [a,b] of cases) { + const x = Buffer.from(a, "binary"), y = Buffer.from(b, "binary"); + console.log(JSON.stringify(a), JSON.stringify(b), x.compare(y), x.equals(y), Buffer.compare(x, y)); +} +console.log("sorted", [Buffer.from("b"), Buffer.from("a"), Buffer.from("ab")].sort(Buffer.compare).map(String).join(",")); +} + +{ +const b = Buffer.alloc(12); +b.writeUIntBE(0x123456, 0, 3); b.writeUIntLE(0x123456, 3, 3); +console.log("hex", b.toString("hex")); +console.log("uintBE", b.readUIntBE(0, 3), "uintLE", b.readUIntLE(3, 3)); +const s = Buffer.alloc(6); s.writeIntBE(-1000, 0, 3); s.writeIntLE(-1000, 3, 3); +console.log("intBE", s.readIntBE(0, 3), "intLE", s.readIntLE(3, 3), s.toString("hex")); +for (const w of [1,2,3,4,5,6]) { const t = Buffer.alloc(6); t.writeUIntBE(255, 0, w); console.log("w"+w, t.readUIntBE(0, w), t.toString("hex")); } +const sw = Buffer.from([1,2,3,4,5,6,7,8]); +console.log("swap16", Buffer.from(sw).swap16().toString("hex")); +console.log("swap32", Buffer.from(sw).swap32().toString("hex")); +console.log("swap64", Buffer.from(sw).swap64().toString("hex")); +try { Buffer.from([1,2,3]).swap16(); } catch (e) { console.log("odd swap ->", e.constructor.name); } +try { b.readUIntBE(0, 7); } catch (e) { console.log("width 7 ->", e.constructor.name); } +} + +{ +const b = Buffer.alloc(10, 0x2e); +console.log("wrote", b.write("hello"), b.toString()); +const c = Buffer.alloc(10, 0x2e); +console.log("offset", c.write("hi", 3), c.toString()); +const d = Buffer.alloc(4, 0x2e); +console.log("truncated", d.write("hello"), d.toString()); +const e = Buffer.alloc(10, 0x2e); +console.log("length cap", e.write("hello", 0, 3), e.toString()); +const f = Buffer.alloc(3, 0x2e); +console.log("multibyte", f.write("héllo"), JSON.stringify(f.toString())); +const g = Buffer.alloc(8, 0x2e); +console.log("encoding arg", g.write("hey", "utf8"), g.toString()); +} + +{ +const src = Buffer.from([1,2,3]); +const copy = Buffer.from(src); +copy[0] = 99; +console.log("src after writing to the copy:", src[0], "copy:", copy[0]); +const u8 = new Uint8Array([1,2,3]); +const fromU8 = Buffer.from(u8); +fromU8[0] = 42; +console.log("uint8array after:", u8[0], "buffer:", fromU8[0]); +const ab = new ArrayBuffer(3); +const view = Buffer.from(ab); +view[0] = 7; +console.log("arraybuffer view shares:", new Uint8Array(ab)[0]); +} + +const expected = readFileSync(new URL("./node_buffer_numbers.expected", import.meta.url).pathname, "utf8").trimEnd().split("\n"); +let bad = 0; +for (let i = 0; i < Math.max(printed.length, expected.length); i++) { + if (printed[i] === expected[i]) continue; + bad++; + globalThis.console.log("FAIL want " + (expected[i] ?? "(nothing)")); + globalThis.console.log(" got " + (printed[i] ?? "(nothing)")); +} +globalThis.console.log(bad === 0 ? `Buffer: ${printed.length} answers identical to Node` : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_buffer_units.expected b/tests/fixtures/node_buffer_units.expected new file mode 100644 index 0000000..9d54a0f --- /dev/null +++ b/tests/fixtures/node_buffer_units.expected @@ -0,0 +1,112 @@ +enc latin1 "" "" +enc ascii "" "" +enc binary "" "" +enc utf16le "" "" +enc ucs2 "" "" +enc utf-16le "" "" +enc latin1 "hello" 68656c6c6f "hello" +enc ascii "hello" 68656c6c6f "hello" +enc binary "hello" 68656c6c6f "hello" +enc utf16le "hello" 680065006c006c006f00 "hello" +enc ucs2 "hello" 680065006c006c006f00 "hello" +enc utf-16le "hello" 680065006c006c006f00 "hello" +enc latin1 "héllo" 68e96c6c6f "héllo" +enc ascii "héllo" 68e96c6c6f "hillo" +enc binary "héllo" 68e96c6c6f "héllo" +enc utf16le "héllo" 6800e9006c006c006f00 "héllo" +enc ucs2 "héllo" 6800e9006c006c006f00 "héllo" +enc utf-16le "héllo" 6800e9006c006c006f00 "héllo" +enc latin1 "日本語" e52c9e "å,ž" +enc ascii "日本語" e52c9e "e,\u001e" +enc binary "日本語" e52c9e "å,ž" +enc utf16le "日本語" e5652c679e8a "日本語" +enc ucs2 "日本語" e5652c679e8a "日本語" +enc utf-16le "日本語" e5652c679e8a "日本語" +enc latin1 "🎉" 3c89 "<‰" +enc ascii "🎉" 3c89 "<\t" +enc binary "🎉" 3c89 "<‰" +enc utf16le "🎉" 3cd889df "🎉" +enc ucs2 "🎉" 3cd889df "🎉" +enc utf-16le "🎉" 3cd889df "🎉" +enc latin1 "a\ud800b" 610062 "a\u0000b" +enc ascii "a\ud800b" 610062 "a\u0000b" +enc binary "a\ud800b" 610062 "a\u0000b" +enc utf16le "a\ud800b" 610000d86200 "a\ud800b" +enc ucs2 "a\ud800b" 610000d86200 "a\ud800b" +enc utf-16le "a\ud800b" 610000d86200 "a\ud800b" +enc latin1 "a\udc00b" 610062 "a\u0000b" +enc ascii "a\udc00b" 610062 "a\u0000b" +enc binary "a\udc00b" 610062 "a\u0000b" +enc utf16le "a\udc00b" 610000dc6200 "a\udc00b" +enc ucs2 "a\udc00b" 610000dc6200 "a\udc00b" +enc utf-16le "a\udc00b" 610000dc6200 "a\udc00b" +enc latin1 "ÿĀ￿" ff00ff "ÿ\u0000ÿ" +enc ascii "ÿĀ￿" ff00ff "\u0000" +enc binary "ÿĀ￿" ff00ff "ÿ\u0000ÿ" +enc utf16le "ÿĀ￿" ff000001ffff "ÿĀ￿" +enc ucs2 "ÿĀ￿" ff000001ffff "ÿĀ￿" +enc utf-16le "ÿĀ￿" ff000001ffff "ÿĀ￿" +enc latin1 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 78787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +enc ascii "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 78787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +enc binary "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 78787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +enc utf16le "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 7800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +enc ucs2 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 7800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +enc utf-16le "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 7800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800780078007800 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +dec latin1 "" +dec ascii "" +dec binary "" +dec utf16le "" +dec ucs2 "" +dec latin1 00 "\u0000" +dec ascii 00 "\u0000" +dec binary 00 "\u0000" +dec utf16le 00 "" +dec ucs2 00 "" +dec latin1 41c1 "AÁ" +dec ascii 41c1 "AA" +dec binary 41c1 "AÁ" +dec utf16le 41c1 "셁" +dec ucs2 41c1 "셁" +dec latin1 00d8 "\u0000Ø" +dec ascii 00d8 "\u0000X" +dec binary 00d8 "\u0000Ø" +dec utf16le 00d8 "\ud800" +dec ucs2 00d8 "\ud800" +dec latin1 ffff41 "ÿÿA" +dec ascii ffff41 "A" +dec binary ffff41 "ÿÿA" +dec utf16le ffff41 "￿" +dec ucs2 ffff41 "￿" +dec latin1 e9 "é" +dec ascii e9 "i" +dec binary e9 "é" +dec utf16le e9 "" +dec ucs2 e9 "" +dec latin1 010203 "\u0001\u0002\u0003" +dec ascii 010203 "\u0001\u0002\u0003" +dec binary 010203 "\u0001\u0002\u0003" +dec utf16le 010203 "ȁ" +dec ucs2 010203 "ȁ" +from view copies 1 090203 true +from a slice of a view 0203 +from a float array 0102 +from an empty view 0 +from an array 412cff02 +from an array-like 010203 +concat abcdefghij +concat short abcd +concat long 20 6162636465666768696a00000000000000000000 +concat empty 0 +concat is a Buffer true +concat of views 010203 +lenient hex "4a4bÿ41" 4a4b +lenient hex "zz" +lenient hex "4a🎉" 4a +lenient hex "4A4b" 4a4b +lenient hex "4a4" 4a +lenient base64 "QUJDÿ" 414243 +lenient base64 "QU JD" 414243 +lenient base64 "QUJD🎉QUJD" 414243414243 +lenient base64 "QUJ=" 4142 +lenient base64url "-_8=" fbff +lenient base64 "Q\nUJD" 414243 diff --git a/tests/fixtures/node_buffer_units.mjs b/tests/fixtures/node_buffer_units.mjs new file mode 100644 index 0000000..9b32808 --- /dev/null +++ b/tests/fixtures/node_buffer_units.mjs @@ -0,0 +1,64 @@ +// latin1, Node's 7-bit "ascii" and utf16le in both directions, now native. +// The interesting inputs are the ones where a code unit is not a byte: a +// lone surrogate, an emoji's pair, anything above 0xff. +import { readFileSync } from "node:fs"; +const lines = []; +const log = (...a) => { lines.push(a.join(" ")); }; + +const strings = ["", "hello", "héllo", "日本語", "🎉", "a\ud800b", "a\udc00b", "ÿĀ￿", "x".repeat(100)]; +for (const s of strings) + for (const enc of ["latin1", "ascii", "binary", "utf16le", "ucs2", "utf-16le"]) + log("enc", enc, JSON.stringify(s), Buffer.from(s, enc).toString("hex"), + JSON.stringify(Buffer.from(s, enc).toString(enc))); +for (const hex of ["", "00", "41c1", "00d8", "ffff41", "e9", "010203"]) + for (const enc of ["latin1", "ascii", "binary", "utf16le", "ucs2"]) + log("dec", enc, hex, JSON.stringify(Buffer.from(hex, "hex").toString(enc))); + +// Buffer.from of a view copies, and the copy must not share bytes with the +// original. A Float64Array is not raw bytes to Node: it truncates each +// element to a byte. +{ + const src = new Uint8Array([1, 2, 3]); + const copy = Buffer.from(src); + copy[0] = 9; + log("from view copies", src[0], copy.toString("hex"), Buffer.isBuffer(copy)); + log("from a slice of a view", Buffer.from(new Uint8Array([1, 2, 3, 4]).subarray(1, 3)).toString("hex")); + log("from a float array", Buffer.from(new Float64Array([1, 2])).toString("hex")); + log("from an empty view", Buffer.from(new Uint8Array()).length); + log("from an array", Buffer.from([65, 300, -1, 2.7]).toString("hex")); + log("from an array-like", Buffer.from({ length: 3, 0: 1, 1: 2, 2: 3 }).toString("hex")); +} + +// Buffer.concat, which is native now: with and without a length, a length +// that cuts the parts short, one that runs past them and leaves zeroes, and +// an empty list. +{ + const parts = [Buffer.from("abc"), Buffer.from("de"), Buffer.from("fghij")]; + log("concat", Buffer.concat(parts).toString()); + log("concat short", Buffer.concat(parts, 4).toString()); + log("concat long", Buffer.concat(parts, 20).length, Buffer.concat(parts, 20).toString("hex")); + log("concat empty", Buffer.concat([]).length, Buffer.concat([], 3).toString("hex")); + log("concat is a Buffer", Buffer.isBuffer(Buffer.concat(parts))); + log("concat of views", Buffer.concat([new Uint8Array([1, 2]), new Uint8Array([3])]).toString("hex")); +} + +// Hex and base64 the way Node reads them: it stops at the first pair that +// is not hex, skips anything outside the base64 alphabet, and reads the +// string a byte at a time -- so a code unit above 0xff is truncated, not +// skipped, and an emoji's low half ends a base64 string. +for (const [enc, str] of [["hex", "4a4b\u00ff41"], ["hex", "zz"], ["hex", "4a\ud83c\udf89"], + ["hex", "4A4b"], ["hex", "4a4"], ["base64", "QUJD\u00ff"], + ["base64", "QU JD"], ["base64", "QUJD\ud83c\udf89QUJD"], + ["base64", "QUJ="], ["base64url", "-_8="], ["base64", "Q\nUJD"]]) + log("lenient", enc, JSON.stringify(str), Buffer.from(str, enc).toString("hex")); + +const expected = readFileSync(new URL("./node_buffer_units.expected", import.meta.url).pathname, "utf8").trimEnd().split("\n"); +let bad = 0; +for (let i = 0; i < Math.max(lines.length, expected.length); i++) { + if (lines[i] === expected[i]) continue; + bad++; + console.log("FAIL want " + (expected[i] ?? "(nothing)")); + console.log(" got " + (lines[i] ?? "(nothing)")); +} +console.log(bad === 0 ? `Buffer code units: ${lines.length} answers identical to Node` : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_builtin_require.mjs b/tests/fixtures/node_builtin_require.mjs new file mode 100644 index 0000000..7aee39b --- /dev/null +++ b/tests/fixtures/node_builtin_require.mjs @@ -0,0 +1,38 @@ +// require() of a builtin, now a native table lookup. Every specifier Node +// answers here, with and without the node: prefix, plus the two sub-modules +// and the one that must not resolve. +import module from "node:module"; +const require = module.createRequire(import.meta.url); +let bad = 0; +const check = (name, got, want) => { + const ok = got === want; + if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + name + " got=" + got + " want=" + want); +}; + +const names = ["assert", "assert/strict", "buffer", "crypto", "events", "fs", + "fs/promises", "http", "module", "net", "os", "path", "perf_hooks", + "process", "querystring", "stream", "stream/promises", + "string_decoder", "timers", "timers/promises", "tty", "url", + "util", "zlib"]; +for (const n of names) { + for (const spec of [n, "node:" + n]) { + const m = require(spec); + check("require " + spec, m !== undefined && m !== null, true); + } +} +check("buffer carries Buffer", require("buffer").Buffer === Buffer, true); +// The default export is this runtime's own convenience for `import buf from +// "node:buffer"`; Node has no such property on the CommonJS object. +check("buffer default", typeof require("node:buffer").default, typeof Buffer); +check("same object twice", require("fs") === require("node:fs"), true); +check("promises sub-module", typeof require("timers/promises").setTimeout, "function"); + +let code = ""; +try { require("definitely-not-a-builtin"); } catch (e) { code = e.code; } +check("unknown module", code, "MODULE_NOT_FOUND"); +check("isBuiltin known", module.isBuiltin("node:fs"), true); +check("isBuiltin unknown", module.isBuiltin("express"), false); + +console.log(bad === 0 ? "node:module require: all builtins resolve" : "FAILURES: " + bad); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_builtins.mjs b/tests/fixtures/node_builtins.mjs index 431dee4..bafaacc 100644 --- a/tests/fixtures/node_builtins.mjs +++ b/tests/fixtures/node_builtins.mjs @@ -1,5 +1,5 @@ import util from "node:util"; -import { promisify, format, inherits, types } from "node:util"; +import { promisify, callbackify, format, inherits, types } from "node:util"; import os from "node:os"; import qs from "node:querystring"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -21,6 +21,12 @@ p("format extra args", format("a", 1, "b")); // util.inherits function Base(){} Base.prototype.hi = () => "hi"; function Derived(){} inherits(Derived, Base); +// inherits is native: it sets super_ and reparents the prototype, and it +// refuses anything that is not a pair of constructors. +p("inherits refuses a non-function", (() => { try { inherits(Derived, {}); return "no"; } + catch (e) { return e.constructor.name; } })()); +p("super_ is writable", (() => { Derived.super_ = null; const ok = Derived.super_ === null; + inherits(Derived, Base); return ok && Derived.super_ === Base; })()); p("inherits", [new Derived().hi(), Derived.super_ === Base]); // util.types @@ -39,9 +45,47 @@ p("qs parse", { ...qs.parse("a=1&b=two&a=3") }); p("qs stringify", qs.stringify({ a: 1, b: ["x","y"] })); p("qs roundtrip", { ...qs.parse(qs.stringify({ k: "a b&c" })) }); +// util.promisify is native: it keeps the first value a callback reports, +// keeps `this`, keeps the function's name, and lets a synchronous throw out. +p("promisify value", await promisify((x, cb) => cb(null, x * 2))(21)); +p("promisify first value only", await promisify((cb) => cb(null, 1, 2, 3))()); +p("promisify no value", await promisify((cb) => cb(null))()); +p("promisify rejects", await promisify((cb) => cb(new Error("nope")))().catch((e) => e.message)); +p("promisify keeps this", await (() => { const o = { v: 5, m(cb) { cb(null, this.v); } }; + o.p = promisify(o.m); return o.p(); })()); +p("promisify keeps the name", promisify(function original(cb) { cb(null); }).name); +p("promisify turns a throw into a rejection", + await promisify(() => { throw new TypeError("sync"); })().then(() => "no", (e) => e.constructor.name)); +p("promisify refuses a non-function", (() => { try { promisify(42); return "no"; } + catch (e) { return e.constructor.name; } })()); + +// util.callbackify is native: the value, an error, a falsy rejection wrapped +// the way Node wraps it, `this`, and a resolved undefined. +p("callbackify", await new Promise((r) => callbackify(async (x) => x * 2)(21, (e, v) => r([e, v])))); +p("callbackify error", await new Promise((r) => callbackify(async () => { throw new Error("bad"); })((e) => r(e.message)))); +p("callbackify falsy rejection", await new Promise((r) => + callbackify(async () => { throw null; })((e) => r([e.message, e.reason])))); +p("callbackify keeps this", await new Promise((r) => + callbackify(async function () { return this.v; }).call({ v: 5 }, (e, v) => r(v)))); +p("callbackify undefined", await new Promise((r) => callbackify(async () => undefined)((e, v) => r([e, v])))); + // url p("fileURLToPath", fileURLToPath("file:///tmp/x%20y.txt")); p("pathToFileURL", String(pathToFileURL("/tmp/a b.txt"))); +// fileURLToPath is native now: the scheme check, an empty localhost host, +// percent-decoding including a multi-byte character, and a URL object. +p("file url root", fileURLToPath("file:///")); +p("file url localhost", fileURLToPath("file://localhost/x/y")); +p("file url utf8", fileURLToPath("file:///a/%C3%A9.txt")); +p("file url plus", fileURLToPath("file:///a+b")); +p("file url object", fileURLToPath(new URL("file:///from/object"))); +p("file url rejects http", (() => { try { fileURLToPath("http://x/y"); return "no"; } + catch (e) { return e.constructor.name; } })()); +p("round trip", fileURLToPath(pathToFileURL("/tmp/a b.txt"))); +// pathToFileURL escapes what a URL would otherwise read as structure, and +// leaves alone what it would not -- brackets included, which a URL keeps. +for (const raw of ["/a b/c.txt", "/a?b#c", "/\u00e9/\u65e5\u672c", "/a%b", "/", "/a'b(c)", "/a[b]c", "/a+b&c=d,e;f"]) + p("path url " + raw, [String(pathToFileURL(raw)), fileURLToPath(pathToFileURL(raw))]); // assert p("assert ok", (()=>{ assert(true); assert.ok(1); return "passed" })()); diff --git a/tests/fixtures/node_format.expected b/tests/fixtures/node_format.expected new file mode 100644 index 0000000..a77caf4 --- /dev/null +++ b/tests/fixtures/node_format.expected @@ -0,0 +1,19 @@ +"hello" +"hello world" +"1 + 2 = 3" +"4" +"4.5" +"{\"a\":1}" +"{ a: 1 }" +"{ a: { b: { c: 1 } } }" +"%%" +"% x" +"%z 1" +"one and %s" +"a b c" +"count: NaN" +"null" +"10n" +" red" +"1 2" +"trailing { x: 1 }" diff --git a/tests/fixtures/node_format.mjs b/tests/fixtures/node_format.mjs new file mode 100644 index 0000000..ca5e93d --- /dev/null +++ b/tests/fixtures/node_format.mjs @@ -0,0 +1,38 @@ +// util.format, which is C now (js_util_format in src/node.c) apart from the +// two cases that need util.inspect. The expected output is Node's. +import util from "node:util"; +import { readFileSync } from "node:fs"; + +const printed = []; +const console = { log: (...args) => printed.push(args.join(" ")) }; +const f = util.format; +console.log(JSON.stringify(f("hello"))); +console.log(JSON.stringify(f("%s world", "hello"))); +console.log(JSON.stringify(f("%d + %d = %d", 1, 2, 3))); +console.log(JSON.stringify(f("%i", 4.9))); +console.log(JSON.stringify(f("%f", 4.5))); +console.log(JSON.stringify(f("%j", { a: 1 }))); +console.log(JSON.stringify(f("%s", { a: 1 }))); +console.log(JSON.stringify(f("%o", { a: { b: { c: 1 } } }))); +console.log(JSON.stringify(f("%%"))); +console.log(JSON.stringify(f("%% %s", "x"))); +console.log(JSON.stringify(f("%z", 1))); +console.log(JSON.stringify(f("%s and %s", "one"))); +console.log(JSON.stringify(f("a", "b", "c"))); +console.log(JSON.stringify(f("count: %d", "12abc"))); +console.log(JSON.stringify(f("%s", null), f("%s", undefined))); +console.log(JSON.stringify(f("%d", 10n))); +console.log(JSON.stringify(f("%c red", "color: red"))); +console.log(JSON.stringify(f(1, 2))); +console.log(JSON.stringify(f("trailing", { x: 1 }))); + +const expected = readFileSync(new URL("./node_format.expected", import.meta.url).pathname, "utf8").trimEnd().split("\n"); +let bad = 0; +for (let i = 0; i < Math.max(printed.length, expected.length); i++) { + if (printed[i] === expected[i]) continue; + bad++; + globalThis.console.log("FAIL want " + (expected[i] ?? "(nothing)")); + globalThis.console.log(" got " + (printed[i] ?? "(nothing)")); +} +globalThis.console.log(bad === 0 ? `util.format: ${printed.length} answers identical to Node` : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_hmac.expected b/tests/fixtures/node_hmac.expected new file mode 100644 index 0000000..8e0e0e1 --- /dev/null +++ b/tests/fixtures/node_hmac.expected @@ -0,0 +1,34 @@ +sha1 "k" "message" 2d795a5f313a0bb4e42011cbd2063b8a2610df75 +sha1 "" "empty key" 60e4e201d6e1b6348ba256f68186bddb58f3a938 +sha1 "aaaaaaaaaaaa" "long key" ed7c878aaf3d3afa40b8736ebb83f94360d1b120 +sha1 "k" "" 3a84a218ee6665209bb70e84525dd837645a1965 +sha256 "k" "message" 9831a5afefa770d5fe6c985f6c343796ca3d27651d69e24864f1f9948debfacf +sha256 "" "empty key" bf6e6a0f34cd9fdf69c67b8c62969c87cd4f783d4665217df03f062dc4a0406e +sha256 "aaaaaaaaaaaa" "long key" f7b340fe6c42e1512d7a40dff53513cf693899fe598a9e248b27412b10f8c245 +sha256 "k" "" 8bb990c40a7d61cb97597a942125025be50ac8beb74436e3735b98893a7f6620 +sha512 "k" "message" 64906709a2715107346e6c90d94d00e77d240f50adffb0d08cb204f010a80211af7947ccfd3ed9df99310e464cc6201a8fc03e0f00ecd761e4e9f82ae6fcd13d +sha512 "" "empty key" a2216fd9ef34dc16c8e559c675f255889c380e3f8c7d19c6836ec6ed3bb7eff126d2cc221cb38ea33883cdf9c365d8248049dfe67e6781aed6ef2f48856cf2e5 +sha512 "aaaaaaaaaaaa" "long key" 129faf68e2a065737a2052d8654e55f199f7e306205ebb7e9236b9532f8efdbfdb40304bb9a4c26ad4afefa9e15efe2bd17de54b310e3ac586710f4caa6db861 +sha512 "k" "" 893ccbf5d0b335fcda6f625e4a59055a364d75a9251589428750782c116830a1af455efe1094c1901d0e8fd5beb6df64c6d5fe1c6f09be6ef47fb3987260cdd8 +md5 "k" "message" 830e89d40ff8cacbb791807541e5b78e +md5 "" "empty key" 4ba1126c25c31c3c5c431c2c68784b6b +md5 "aaaaaaaaaaaa" "long key" 80e6d83819da34d75e633702a805c094 +md5 "k" "" cd32bedd46aa63cffa3023f050fc78e3 +streamed NC5RnOCtbAOja5jus/HRMNtIE7nfTRFg7aSI1xLceO4= +equal true +differ false +mismatch -> RangeError +hash hex "deadbeef" 5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953 +hmac hex "deadbeef" vnR0a3D0RWFcGEI8e8BOnmC59Zx/no4aDlvGyHVrpys= +hash hex "DEADBEEF" 5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953 +hmac hex "DEADBEEF" vnR0a3D0RWFcGEI8e8BOnmC59Zx/no4aDlvGyHVrpys= +hash base64 "QUJD" b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78 +hmac base64 "QUJD" R2EoX7/mTM/FUNUp5uqdKly4ge7dOi57M7ji6ypTwbI= +hash base64url "QUJD" b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78 +hmac base64url "QUJD" R2EoX7/mTM/FUNUp5uqdKly4ge7dOi57M7ji6ypTwbI= +hash latin1 "ÿþ" b3d510ef04275ca8e698e5b3cbb0ece3949ef9252f0cdc839e9ee347409a2209 +hmac latin1 "ÿþ" SR8auta1W/HKyIqhNX4m9h49m89Do9vWO3yTIhdbcDk= +hash utf8 "abc" ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad +hmac utf8 "abc" nBluMtwBdfhvSxy4konWYZ3mvuaZ5MN45oMJ7Zehpqs= +hash "abc" ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad +hmac "abc" nBluMtwBdfhvSxy4konWYZ3mvuaZ5MN45oMJ7Zehpqs= diff --git a/tests/fixtures/node_hmac.mjs b/tests/fixtures/node_hmac.mjs new file mode 100644 index 0000000..ff85858 --- /dev/null +++ b/tests/fixtures/node_hmac.mjs @@ -0,0 +1,37 @@ +// HMAC and timingSafeEqual, which are OpenSSL's now (sxn_hmac and +// sxn_timing_safe_equal in src/network.c). The expected digests are Node's. +import crypto from "node:crypto"; +import { readFileSync } from "node:fs"; + +const printed = []; +const console = { log: (...args) => printed.push(args.join(" ")) }; +for (const algo of ["sha1", "sha256", "sha512", "md5"]) { + for (const [key, data] of [["k", "message"], ["", "empty key"], ["a".repeat(200), "long key"], ["k", ""]]) { + const h = crypto.createHmac(algo, key).update(data).digest("hex"); + console.log(algo, JSON.stringify(key.slice(0, 12)), JSON.stringify(data), h); + } +} +const streamed = crypto.createHmac("sha256", "k").update("a").update("b").update("c").digest("base64"); +console.log("streamed", streamed); +console.log("equal", crypto.timingSafeEqual(Buffer.from("abc"), Buffer.from("abc"))); +console.log("differ", crypto.timingSafeEqual(Buffer.from("abc"), Buffer.from("abd"))); +try { crypto.timingSafeEqual(Buffer.from("ab"), Buffer.from("abc")); } catch (e) { console.log("mismatch ->", e.constructor.name); } + +// Input encodings: update() reads hex, base64 and latin1 through Buffer's +// native readers now, and every one of them has to hash the same bytes. +for (const [text, enc] of [["deadbeef", "hex"], ["DEADBEEF", "hex"], ["QUJD", "base64"], + ["QUJD", "base64url"], ["\u00ff\u00fe", "latin1"], ["abc", "utf8"], ["abc", undefined]]) { + console.log("hash", enc, JSON.stringify(text), crypto.createHash("sha256").update(text, enc).digest("hex")); + console.log("hmac", enc, JSON.stringify(text), crypto.createHmac("sha256", "key").update(text, enc).digest("base64")); +} + +const expected = readFileSync(new URL("./node_hmac.expected", import.meta.url).pathname, "utf8").trimEnd().split("\n"); +let bad = 0; +for (let i = 0; i < Math.max(printed.length, expected.length); i++) { + if (printed[i] === expected[i]) continue; + bad++; + globalThis.console.log("FAIL want " + (expected[i] ?? "(nothing)")); + globalThis.console.log(" got " + (printed[i] ?? "(nothing)")); +} +globalThis.console.log(bad === 0 ? `node:crypto hmac: ${printed.length} answers identical to Node` : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_http.mjs b/tests/fixtures/node_http.mjs index 4f4e0b2..6225431 100644 --- a/tests/fixtures/node_http.mjs +++ b/tests/fixtures/node_http.mjs @@ -6,7 +6,34 @@ const check = (n, got, want) => { const ok = JSON.stringify(got) === JSON.string const server = http.createServer((req, res) => { const url = req.url; - if (url === "/plumbing") { + if (url === "/read-body") { + // The body is pushed on the first read, never before, and 'end' marks + // the request complete -- both of which are shared native functions now + // rather than closures built per request. + let n = 0; + const early = req.complete; + req.on("data", (c) => { n += c.length; }); + req.on("end", () => res.end(JSON.stringify({ n, early, complete: req.complete }))); + } else if (url === "/socket") { + // The socket a request carries: the fields on-finished and finalhandler + // read, the setters they chain off, and destroy() emitting 'close'. + const sock = req.socket; + const before = { readable: sock.readable, writable: sock.writable, destroyed: sock.destroyed, + addr: typeof sock.remoteAddress, sameAsConnection: req.connection === sock, + chains: sock.setNoDelay(true) === sock && sock.setKeepAlive(true) === sock && + sock.setTimeout(0) === sock }; + // destroy() is not exercised here: under Node it really does close the + // connection, and this response still has to reach the client. + res.end(JSON.stringify(before)); + } else if (url === "/body-shapes") { + // How the written chunks are joined into one body: one string, several + // strings, bytes only, and a mix of the two. + if (req.headers["x-shape"] === "multi") { res.write("a"); res.write("b"); res.end("c"); } + else if (req.headers["x-shape"] === "mixed") { res.write("a"); res.end(new Uint8Array([66, 67])); } + else if (req.headers["x-shape"] === "bytes") { res.write(new Uint8Array([65])); res.end(new Uint8Array([66])); } + else if (req.headers["x-shape"] === "empty") res.end(); + else res.end("one"); + } else if (url === "/plumbing") { // The shapes finalhandler and on-finished reach for when they answer a // request nobody read: unpipe on a stream that was never piped, and a // socket they can subscribe to. @@ -36,7 +63,15 @@ const server = http.createServer((req, res) => { }, 0); } else if (url === "/json") { res.writeHead(201, { "content-type": "application/json", "x-a": "1" }); - res.end(JSON.stringify({ method: req.method, ua: !!req.headers["user-agent"] })); + // Header names arrive lowercased whatever the client sent, and + // rawHeaders is the same list flattened into name, value pairs. + // Node keeps the original spelling in rawHeaders, so the pairs are + // matched against req.headers case-insensitively. + let rawOk = req.rawHeaders.length === Object.keys(req.headers).length * 2; + for (let i = 0; rawOk && i < req.rawHeaders.length; i += 2) + rawOk = req.headers[req.rawHeaders[i].toLowerCase()] === req.rawHeaders[i + 1]; + res.end(JSON.stringify({ method: req.method, ua: !!req.headers["user-agent"], + xcase: req.headers["x-mixed-case"], rawOk })); } else if (url === "/chunks") { res.setHeader("content-type", "text/plain"); res.write("one "); @@ -51,8 +86,27 @@ const server = http.createServer((req, res) => { check("hasHeader", res.hasHeader("x-set"), true); check("getHeader", res.getHeader("X-Set"), "yes"); check("getHeaderNames", res.getHeaderNames(), ["x-set"]); + // getHeaders hands back a copy, in the order the names were set. + res.setHeader("x-two", "2"); + const snapshot = res.getHeaders(); + res.setHeader("x-three", "3"); + check("getHeaders is a copy", snapshot, { "x-set": "yes", "x-two": "2" }); + check("getHeaders sees the third", res.getHeaders()["x-three"], "3"); + check("names in order", res.getHeaderNames(), ["x-set", "x-two", "x-three"]); + res.removeHeader("x-two"); + res.removeHeader("x-three"); + // The name is lowercased whatever spelling it arrives in, and hasHeader + // asks about own properties only. + res.setHeader("X-Mixed", "1"); + check("mixed case set", res.getHeader("x-mixed"), "1"); + check("hasHeader is own only", res.hasHeader("constructor"), false); + check("missing header", res.getHeader("x-absent"), undefined); + check("setHeader chains", res.setHeader("x-chain", "c") === res, true); res.removeHeader("x-set"); + res.removeHeader("X-Mixed"); + res.removeHeader("x-chain"); check("removeHeader", res.hasHeader("x-set"), false); + check("removeHeader mixed case", res.getHeaderNames(), []); res.end("hdr"); } else if (url === "/late") { setTimeout(() => res.end("after a tick"), 20); @@ -66,11 +120,11 @@ await once(server, "listening"); check("address port", server.address().port, 8961); const base = "http://127.0.0.1:8961"; -const j = await fetch(base + "/json"); +const j = await fetch(base + "/json", { headers: { "X-Mixed-Case": "kept" } }); check("status", j.status, 201); check("content-type", j.headers.get("content-type"), "application/json"); check("custom header", j.headers.get("x-a"), "1"); -check("json body", await j.json(), { method: "GET", ua: true }); +check("json body", await j.json(), { method: "GET", ua: true, xcase: "kept", rawOk: true }); const c = await fetch(base + "/chunks"); check("multiple writes", await c.text(), "one two"); @@ -90,6 +144,18 @@ const lb = await (await fetch(base + "/late-body", { method: "POST", body: "defe check("body survives a late listener", lb, { complete: false, sockReadable: true, body: "deferred", doneAfter: true }); +check("read body", await (await fetch(base + "/read-body", { method: "POST", body: "hello body" })).json(), + { n: 10, early: false, complete: true }); +check("read empty body", await (await fetch(base + "/read-body")).json(), + { n: 0, early: false, complete: true }); + +check("socket", await (await fetch(base + "/socket")).json(), + { readable: true, writable: true, destroyed: false, addr: "string", + sameAsConnection: true, chains: true }); + +for (const [shape, want] of [["one", "one"], ["multi", "abc"], ["mixed", "aBC"], ["bytes", "AB"], ["empty", ""]]) + check("body " + shape, await (await fetch(base + "/body-shapes", { headers: { "x-shape": shape } })).text(), want); + const pl = await (await fetch(base + "/plumbing")).json(); check("request plumbing", pl, { unpipe: true, socketOn: true, socketWritable: true, heard: true }); diff --git a/tests/fixtures/node_inspect.mjs b/tests/fixtures/node_inspect.mjs new file mode 100644 index 0000000..20fd6dd --- /dev/null +++ b/tests/fixtures/node_inspect.mjs @@ -0,0 +1,50 @@ +// util.inspect, native now. What it prints is this runtime's own shape +// rather than Node's, so these are pinned here: every kind of value, the +// depth limit, a cycle, and the key quoting. +import util from "node:util"; +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + n + " got=" + got + (ok ? "" : " want=" + want)); }; + +check("null", util.inspect(null), "null"); +check("undefined", util.inspect(undefined), "undefined"); +check("number", util.inspect(42), "42"); +check("negative zero", util.inspect(-0), "0"); +check("NaN", util.inspect(NaN), "NaN"); +check("bigint", util.inspect(10n), "10n"); +check("boolean", util.inspect(false), "false"); +check("bare string", util.inspect("plain"), "plain"); +check("string in a container", util.inspect({ s: 'a"b' }), '{ s: "a\\"b" }'); +check("symbol", util.inspect(Symbol("s")), "Symbol(s)"); +check("named function", util.inspect(function named() {}), "[Function: named]"); +check("anonymous function", util.inspect(() => {}), "[Function: anonymous]"); +check("date", util.inspect(new Date(0)), "1970-01-01T00:00:00.000Z"); +check("invalid date", util.inspect(new Date(NaN)), "Invalid Date"); +check("regexp", util.inspect(/ab+c/gi), "/ab+c/gi"); +check("empty array", util.inspect([]), "[]"); +check("array", util.inspect([1, 2, 3]), "[ 1, 2, 3 ]"); +check("holes are undefined", util.inspect([1, , 3]), "[ 1, undefined, 3 ]"); +check("empty object", util.inspect({}), "{}"); +check("object", util.inspect({ a: 1, b: "x" }), '{ a: 1, b: "x" }'); +check("keys that need quotes", util.inspect({ "b c": 1, $d: 2, _e: 3 }), '{ "b c": 1, $d: 2, _e: 3 }'); +check("numeric keys come first", util.inspect({ b: 1, 2: 2 }), '{ "2": 2, b: 1 }'); +check("empty map", util.inspect(new Map()), "Map(0) {}"); +check("map", util.inspect(new Map([["k", 1]])), 'Map(1) { "k" => 1 }'); +check("empty set", util.inspect(new Set()), "Set(0) {}"); +check("set", util.inspect(new Set([1, "a"])), 'Set(2) { 1, "a" }'); +check("typed array", util.inspect(new Uint8Array([1, 2, 3])), "Uint8Array(3) [ 1, 2, 3 ]"); +check("float array", util.inspect(new Float64Array([1.5])), "Float64Array(1) [ 1.5 ]"); +check("depth stops", util.inspect({ a: { b: { c: { d: 1 } } } }), "{ a: { b: { c: [Object] } } }"); +check("deeper on request", util.inspect({ a: { b: { c: { d: 1 } } } }, { depth: 4 }), "{ a: { b: { c: { d: 1 } } } }"); +check("array depth stops", util.inspect([1, [2, [3, [4]]]]), "[ 1, [ 2, [ 3, [Array] ] ] ]"); +{ + const cycle = { name: "c" }; + cycle.self = cycle; + check("cycle", util.inspect(cycle), '{ name: "c", self: [Circular *1] }'); + const shared = { x: 1 }; + check("shared is not a cycle", util.inspect({ l: shared, r: shared }), "{ l: { x: 1 }, r: { x: 1 } }"); +} +check("error carries its stack", util.inspect(new Error("boom")).startsWith("Error: boom"), true); +check("isDeepStrictEqual still works", util.isDeepStrictEqual({ a: [1] }, { a: [1] }), true); +console.log(bad === 0 ? "util.inspect: shapes hold" : "FAILURES: " + bad); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_isip.expected b/tests/fixtures/node_isip.expected new file mode 100644 index 0000000..652e014 --- /dev/null +++ b/tests/fixtures/node_isip.expected @@ -0,0 +1,31 @@ +"1.2.3.4" isIP=4 v4=true v6=false +"0.0.0.0" isIP=4 v4=true v6=false +"255.255.255.255" isIP=4 v4=true v6=false +"256.1.1.1" isIP=0 v4=false v6=false +"01.2.3.4" isIP=0 v4=false v6=false +"1.2.3" isIP=0 v4=false v6=false +"1.2.3.4.5" isIP=0 v4=false v6=false +"" isIP=0 v4=false v6=false +" " isIP=0 v4=false v6=false +"1.2.3.04" isIP=0 v4=false v6=false +"::1" isIP=6 v4=false v6=true +"::" isIP=6 v4=false v6=true +"fe80::1" isIP=6 v4=false v6=true +"fe80::1%eth0" isIP=6 v4=false v6=true +"2001:db8::8a2e:370:7334" isIP=6 v4=false v6=true +"::ffff:1.2.3.4" isIP=6 v4=false v6=true +"1::2::3" isIP=0 v4=false v6=false +"abcd" isIP=0 v4=false v6=false +"1.2.3.4 " isIP=0 v4=false v6=false +" 1.2.3.4" isIP=0 v4=false v6=false +"%eth0" isIP=0 v4=false v6=false +"::1%" isIP=0 v4=false v6=false +"12345::" isIP=0 v4=false v6=false +"0:0:0:0:0:0:0:1" isIP=6 v4=false v6=true +"1.2.3.-4" isIP=0 v4=false v6=false +"1.2.3.4/24" isIP=0 v4=false v6=false +"0x1.2.3.4" isIP=0 v4=false v6=false +"999.999.999.999" isIP=0 v4=false v6=false +"::ffff:0:0" isIP=6 v4=false v6=true +"fe80::1%0" isIP=6 v4=false v6=true +non-strings 0 0 0 diff --git a/tests/fixtures/node_isip.mjs b/tests/fixtures/node_isip.mjs new file mode 100644 index 0000000..40c7aff --- /dev/null +++ b/tests/fixtures/node_isip.mjs @@ -0,0 +1,24 @@ +// net.isIP, which is the system's address parser now (js_net_is_ip in +// src/node.c) rather than two regexps. The expected answers are Node's. +import net from "node:net"; +import { readFileSync } from "node:fs"; + +const printed = []; +const console = { log: (...args) => printed.push(args.join(" ")) }; +const cases = ["1.2.3.4", "0.0.0.0", "255.255.255.255", "256.1.1.1", "01.2.3.4", "1.2.3", "1.2.3.4.5", + "", " ", "1.2.3.04", "::1", "::", "fe80::1", "fe80::1%eth0", "2001:db8::8a2e:370:7334", "::ffff:1.2.3.4", + "1::2::3", "abcd", "1.2.3.4 ", " 1.2.3.4", "%eth0", "::1%", "12345::", "0:0:0:0:0:0:0:1", "1.2.3.-4", + "1.2.3.4/24", "0x1.2.3.4", "999.999.999.999", "::ffff:0:0", "fe80::1%0"]; +for (const c of cases) console.log(JSON.stringify(c), "isIP=" + net.isIP(c), "v4=" + net.isIPv4(c), "v6=" + net.isIPv6(c)); +console.log("non-strings", net.isIP(42), net.isIP(null), net.isIP(undefined)); + +const expected = readFileSync(new URL("./node_isip.expected", import.meta.url).pathname, "utf8").trimEnd().split("\n"); +let bad = 0; +for (let i = 0; i < Math.max(printed.length, expected.length); i++) { + if (printed[i] === expected[i]) continue; + bad++; + globalThis.console.log("FAIL want " + (expected[i] ?? "(nothing)")); + globalThis.console.log(" got " + (printed[i] ?? "(nothing)")); +} +globalThis.console.log(bad === 0 ? `net.isIP: ${printed.length} answers identical to Node` : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_new_builtins.mjs b/tests/fixtures/node_new_builtins.mjs new file mode 100644 index 0000000..210ef71 --- /dev/null +++ b/tests/fixtures/node_new_builtins.mjs @@ -0,0 +1,147 @@ +// The builtins added on top of the original 24: what each one actually does, +// not just that it resolves. +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +let bad = 0; +const check = (name, ok, detail) => { + if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + name + (detail === undefined ? "" : " " + detail)); +}; + +const all = require("module").builtinModules; +const base = all.filter((m) => !m.includes("/")); +check("builtin count", base.length === 37, String(base.length)); +for (const name of base) check("resolves " + name, require("node:" + name) !== undefined); +// Every one of them imports as well as requires: the two paths are separate +// (the loader registers a module; require reads a table), and a name that +// only answers to one of them is a bug that hides until someone writes ESM. +for (const name of all) { + const m = await import("node:" + name); + check("imports " + name, m.default !== undefined || Object.keys(m).length > 0); +} + +// child_process: a real process, its output, and its exit status. +const cp = require("node:child_process"); +check("execSync", cp.execSync("echo hello").trim() === "hello"); +check("execSync sees the shell's exit code", (() => { + try { cp.execSync("exit 3"); return false; } catch (e) { return e.status === 3; } +})()); +const sync = cp.spawnSync("/bin/echo", ["a", "b"]); +check("spawnSync stdout", sync.stdout === "a b\n", JSON.stringify(sync.stdout)); +check("spawnSync status", sync.status === 0); +check("spawnSync on a missing file", cp.spawnSync("/no/such/bin", []).error !== undefined); +check("execFileSync", cp.execFileSync("/bin/echo", ["x"]).trim() === "x"); +check("input reaches stdin", cp.spawnSync("/bin/cat", [], { input: "fed in" }).stdout === "fed in"); +check("env is passed through", cp.execSync("echo $SXN_TEST_VAR", { env: { SXN_TEST_VAR: "set" } }).trim() === "set"); +await new Promise((done) => { + cp.exec("echo async", (error, stdout) => { + check("exec callback", error === null && stdout.trim() === "async"); + done(); + }); +}); +await new Promise((done) => { + const child = cp.spawn("/bin/echo", ["streamed"]); + let out = ""; + child.stdout.on("data", (c) => { out += c; }); + child.on("close", (code) => { + check("spawn streams and closes", out.trim() === "streamed" && code === 0); + done(); + }); +}); + +// dns: the system resolver, through libuv. +const dns = require("node:dns"); +await new Promise((done) => dns.lookup("localhost", (e, address) => { + check("dns.lookup", !e && (address === "127.0.0.1" || address === "::1"), address); + done(); +})); +await new Promise((done) => dns.lookup("nothing.invalid", (e) => { + check("dns.lookup on a bad name", e && e.code === "ENOTFOUND"); + done(); +})); +const dnsp = require("node:dns/promises"); +check("dns.promises.lookup", typeof (await dnsp.lookup("localhost")).address === "string"); +check("dns.resolve4", Array.isArray(await new Promise((r) => dns.resolve4("localhost", (e, a) => r(a || []))))); + +// dgram: a real UDP round trip. +const dgram = require("node:dgram"); +await new Promise((done) => { + const server = dgram.createSocket("udp4"); + server.on("message", (msg, rinfo) => { + check("udp message", msg.toString() === "ping" && rinfo.port > 0); + server.close(); + done(); + }); + server.bind(0, () => { + const client = dgram.createSocket("udp4"); + client.send("ping", server.address().port, "127.0.0.1", () => client.close()); + }); +}); + +// vm, v8, punycode, diagnostics_channel: the ones that compute something. +const vm = require("node:vm"); +check("vm.runInThisContext", vm.runInThisContext("40 + 2") === 42); +check("vm.runInNewContext sees the sandbox", vm.runInNewContext("return x * 2", { x: 21 }) === 42); +check("vm.Script", new vm.Script("7 * 6").runInThisContext() === 42); +const v8 = require("node:v8"); +check("v8 heap statistics", v8.getHeapStatistics().used_heap_size > 0); +check("v8 serialize round trip", v8.deserialize(v8.serialize({ a: [1, 2] })).a[1] === 2); +const punycode = require("node:punycode"); +check("punycode.toASCII", punycode.toASCII("münchen.de") === "xn--mnchen-3ya.de"); +check("punycode.toUnicode", punycode.toUnicode("xn--mnchen-3ya.de") === "münchen.de"); +check("punycode round trip", punycode.decode(punycode.encode("räksmörgås")) === "räksmörgås"); +const dc = require("node:diagnostics_channel"); +let published = null; +dc.subscribe("sxn:test", (m) => { published = m; }); +check("channel has subscribers", dc.hasSubscribers("sxn:test")); +dc.channel("sxn:test").publish({ n: 1 }); +check("diagnostics_channel delivers", published && published.n === 1); + +// async_hooks: the store, which is the reason the module is here. +const { AsyncLocalStorage } = require("node:async_hooks"); +const als = new AsyncLocalStorage(); +check("store inside run", als.run({ id: 1 }, () => als.getStore().id) === 1); +check("store is gone after", als.getStore() === undefined); +check("store survives an await", await als.run({ id: 2 }, async () => { + await Promise.resolve(); + return als.getStore().id === 2; +})); + +// readline: lines out of a stream. +const readline = require("node:readline"); +const { Readable } = require("node:stream"); +const lines = []; +for await (const line of readline.createInterface({ input: Readable.from(["one\ntwo\nthree\n"]) })) + lines.push(line); +check("readline splits lines", lines.join("|") === "one|two|three", lines.join("|")); + +// stream/web is the global Web Streams, not a second implementation. +const web = require("node:stream/web"); +check("stream/web is the same ReadableStream", web.ReadableStream === globalThis.ReadableStream); +check("stream/web has the compression streams", web.CompressionStream === globalThis.CompressionStream); + +// The ones that answer a question rather than do work. +check("worker_threads.isMainThread", require("node:worker_threads").isMainThread === true); +check("cluster.isPrimary", require("node:cluster").isPrimary === true); +check("inspector.url", require("node:inspector").url() === undefined); +check("https.request exists", typeof require("node:https").request === "function"); +check("http2 constants", require("node:http2").constants.HTTP2_HEADER_PATH === ":path"); +check("console module logs", typeof require("node:console").log === "function"); +check("constants has fs's", typeof require("node:constants").O_RDONLY === "number"); + +// What is not supported says so rather than failing obscurely. +for (const [name, call] of [ + ["worker_threads.Worker", () => new (require("node:worker_threads").Worker)("x")], + ["cluster.fork", () => require("node:cluster").fork()], + ["tls.connect", () => require("node:tls").connect({})], + ["http2.connect", () => require("node:http2").connect("https://x.dev")], + ["child_process.fork", () => require("node:child_process").fork("x")], + ["https.createServer", () => require("node:https").createServer()], +]) { + let message = ""; + try { call(); } catch (e) { message = e.message; } + check(name + " explains itself", message.includes("not supported"), JSON.stringify(message)); +} + +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/node_next_tick.mjs b/tests/fixtures/node_next_tick.mjs new file mode 100644 index 0000000..e182db9 --- /dev/null +++ b/tests/fixtures/node_next_tick.mjs @@ -0,0 +1,25 @@ +// process.nextTick, native now: it carries up to three arguments directly +// and keeps the rest in an array, so every arity has to arrive intact. +let bad = 0; +const check = (n, got, want) => { const ok = JSON.stringify(got) === JSON.stringify(want); if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + n + " got=" + JSON.stringify(got) + (ok ? "" : " want=" + JSON.stringify(want))); }; + +const seen = []; +process.nextTick(() => seen.push(["none"])); +process.nextTick((a) => seen.push([a]), 1); +process.nextTick((a, b) => seen.push([a, b]), 1, "two"); +process.nextTick((a, b, c) => seen.push([a, b, c]), 1, "two", null); +process.nextTick((...a) => seen.push(a), 1, 2, 3, 4); +process.nextTick((...a) => seen.push(a), 1, 2, 3, 4, 5, 6, 7); +process.nextTick(function () { seen.push([this === undefined]); }); + +let code = ""; +try { process.nextTick(42); } catch (e) { code = e.constructor.name; } +check("a non-function is refused", code, "TypeError"); + +await new Promise((r) => setTimeout(r, 10)); +check("in order, with their arguments", seen, [ + ["none"], [1], [1, "two"], [1, "two", null], [1, 2, 3, 4], [1, 2, 3, 4, 5, 6, 7], [true], +]); +console.log(bad === 0 ? "process.nextTick: arguments and order hold" : "FAILURES: " + bad); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_os_fs.mjs b/tests/fixtures/node_os_fs.mjs new file mode 100644 index 0000000..fd1df63 --- /dev/null +++ b/tests/fixtures/node_os_fs.mjs @@ -0,0 +1,89 @@ +// node:os and the parts of node:fs a server needs. These were stubs -- the +// hostname was "localhost", the memory sizes 0, the CPU list empty, and +// networkInterfaces and stat did not exist at all -- which is worse than +// missing, because nothing can tell a stub from the truth. +import * as os from "node:os"; +import { networkInterfaces, hostname, cpus, totalmem, availableParallelism } from "node:os"; +import { stat, lstat, readFile } from "node:fs/promises"; +import { statSync, createReadStream, existsSync, writeFileSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok?"ok ":"FAIL ") + n + " got=" + JSON.stringify(got) + " want=" + JSON.stringify(want)); }; + +check("hostname is not a guess", hostname() !== "localhost" || hostname().length > 0, true); +check("there is at least one cpu", cpus().length > 0, true); +check("a cpu has a model", typeof cpus()[0].model === "string" && cpus()[0].model.length > 0, true); +check("total memory is real", totalmem() > 1e6, true); +check("free memory is real", os.freemem() > 0, true); +check("parallelism is at least one", availableParallelism() >= 1, true); +check("loadavg has three numbers", os.loadavg().length, 3); +check("uptime is positive", os.uptime() > 0, true); +check("release says something", os.release().length > 0, true); +check("homedir is absolute", os.homedir().startsWith("/") || /^[A-Za-z]:/.test(os.homedir()), true); + +const nets = networkInterfaces(); +check("interfaces is an object", typeof nets === "object" && nets !== null, true); +const all = Object.values(nets).flat(); +check("there is a loopback address", all.some(a => a.internal && a.family === "IPv4"), true); +const one = all.find(a => a.family === "IPv4"); +check("an address has a netmask", typeof one.netmask === "string" && one.netmask.includes("."), true); +check("an address has a cidr", /\/\d+$/.test(one.cidr), true); +check("an address has a mac", /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/.test(one.mac), true); +check("family is the modern spelling", one.family, "IPv4"); + +const self = new URL(import.meta.url).pathname; +const s = await stat(self); +check("stat reports a size", s.size > 0, true); +check("stat knows it is a file", s.isFile(), true); +check("and not a directory", s.isDirectory(), false); +check("mtime is a Date", s.mtime instanceof Date && s.mtime.getTime() > 0, true); +check("statSync agrees", statSync(self).size, s.size); +check("a directory is a directory", statSync(os.tmpdir()).isDirectory(), true); +check("lstat works too", (await lstat(self)).isFile(), true); +// The stat object is built native, on Stats' own prototype, so it has to +// still be a Stats with all four dates agreeing with their millisecond twins. +check("it is a Stats", s.constructor.name, "Stats"); +check("dates match their milliseconds", + ["atime", "mtime", "ctime", "birthtime"].every((k) => s[k] instanceof Date && + Math.abs(s[k].getTime() - s[k + "Ms"]) < 1), true); +check("the numbers are numbers", + ["dev", "ino", "mode", "nlink", "uid", "gid", "size", "blksize", "blocks"] + .every((k) => typeof s[k] === "number"), true); +check("not a directory", s.isDirectory(), false); +check("not a symlink", s.isSymbolicLink(), false); + +// fs/promises.readFile has to hand back the file's own bytes. It used to +// decode as UTF-8 and re-encode, which turned every byte that is not valid +// UTF-8 into the replacement character. +{ + // One fixed name, rewritten each run: this runtime has no unlinkSync yet. + const binary = join(os.tmpdir(), "sxn-readfile-bytes.bin"); + const bytes = Buffer.from([0x00, 0x80, 0xff, 0xfe, 0x20, 0x41]); + writeFileSync(binary, bytes); + const back = await readFile(binary); + check("binary survives", Buffer.from(back).toString("hex"), bytes.toString("hex")); + check("same as the sync read", Buffer.from(readFileSync(binary)).toString("hex"), bytes.toString("hex")); + check("an encoding still gives text", typeof (await readFile(binary, "utf8")), "string"); + writeFileSync(binary, "h\u00e9llo"); + check("a string still writes as text", readFileSync(binary, "utf8"), "h\u00e9llo"); + writeFileSync(binary, new Uint8Array([1, 2, 3])); + check("a plain array of bytes writes too", Buffer.from(readFileSync(binary)).toString("hex"), "010203"); +} +let code = ""; +try { await stat(self + ".missing"); } catch (e) { code = e.code; } +check("a missing file is ENOENT", code, "ENOENT"); + +const chunks = []; +await new Promise((resolve, reject) => { + const rs = createReadStream(self); + rs.on("data", (c) => chunks.push(c)); + rs.on("end", resolve); + rs.on("error", reject); +}); +check("createReadStream reads the file", chunks.reduce((n, c) => n + c.length, 0), s.size); +check("existsSync still works", existsSync(self), true); + +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/node_path.expected b/tests/fixtures/node_path.expected new file mode 100644 index 0000000..3142598 --- /dev/null +++ b/tests/fixtures/node_path.expected @@ -0,0 +1,448 @@ +posix "" n="." d="." b="" e="" a=false +posix "." n="." d="." b="." e="" a=false +posix ".." n=".." d="." b=".." e="" a=false +posix "..." n="..." d="." b="..." e="." a=false +posix "a" n="a" d="." b="a" e="" a=false +posix "a.txt" n="a.txt" d="." b="a.txt" e=".txt" a=false +posix ".hidden" n=".hidden" d="." b=".hidden" e="" a=false +posix "a.." n="a.." d="." b="a.." e="." a=false +posix "a.b.c" n="a.b.c" d="." b="a.b.c" e=".c" a=false +posix "C:" n="C:" d="." b="C:" e="" a=false +posix "C:\\" n="C:\\" d="." b="C:\\" e="" a=false +posix "C:x" n="C:x" d="." b="C:x" e="" a=false +posix "\\" n="\\" d="." b="\\" e="" a=false +posix "\\\\" n="\\\\" d="." b="\\\\" e="" a=false +posix "/" n="/" d="/" b="" e="" a=true +posix "//" n="/" d="/" b="" e="" a=true +posix "\\\\srv\\share" n="\\\\srv\\share" d="." b="\\\\srv\\share" e="" a=false +posix "\\\\srv\\share\\d" n="\\\\srv\\share\\d" d="." b="\\\\srv\\share\\d" e="" a=false +posix "/usr/local" n="/usr/local" d="/usr" b="local" e="" a=true +posix "usr" n="usr" d="." b="usr" e="" a=false +posix "a/b" n="a/b" d="a" b="b" e="" a=false +posix "a\\b" n="a\\b" d="." b="a\\b" e="" a=false +posix "a//b" n="a/b" d="a/" b="b" e="" a=false +posix "a\\\\b" n="a\\\\b" d="." b="a\\\\b" e="" a=false +posix " " n=" " d="." b=" " e="" a=false +posix "a b" n="a b" d="." b="a b" e="" a=false +posix "./x" n="x" d="." b="x" e="" a=false +posix "../y" n="../y" d=".." b="y" e="" a=false +posix "/a/../b" n="/b" d="/a/.." b="b" e="" a=true +posix "C:\\a\\..\\b" n="C:\\a\\..\\b" d="." b="C:\\a\\..\\b" e=".\\b" a=false +posix "x/" n="x/" d="." b="x" e="" a=false +posix "x\\" n="x\\" d="." b="x\\" e="" a=false +posix join "" "x" -> "x" +posix join "" ".." -> ".." +posix join "" "/y" -> "/y" +posix join "" "C:\\z" -> "C:\\z" +posix join "" "" -> "." +posix join "." "x" -> "x" +posix join "." ".." -> ".." +posix join "." "/y" -> "y" +posix join "." "C:\\z" -> "C:\\z" +posix join "." "" -> "." +posix join ".." "x" -> "../x" +posix join ".." ".." -> "../.." +posix join ".." "/y" -> "../y" +posix join ".." "C:\\z" -> "../C:\\z" +posix join ".." "" -> ".." +posix join "..." "x" -> ".../x" +posix join "..." ".." -> "." +posix join "..." "/y" -> ".../y" +posix join "..." "C:\\z" -> ".../C:\\z" +posix join "..." "" -> "..." +posix join "a" "x" -> "a/x" +posix join "a" ".." -> "." +posix join "a" "/y" -> "a/y" +posix join "a" "C:\\z" -> "a/C:\\z" +posix join "a" "" -> "a" +posix join "a.txt" "x" -> "a.txt/x" +posix join "a.txt" ".." -> "." +posix join "a.txt" "/y" -> "a.txt/y" +posix join "a.txt" "C:\\z" -> "a.txt/C:\\z" +posix join "a.txt" "" -> "a.txt" +posix join ".hidden" "x" -> ".hidden/x" +posix join ".hidden" ".." -> "." +posix join ".hidden" "/y" -> ".hidden/y" +posix join ".hidden" "C:\\z" -> ".hidden/C:\\z" +posix join ".hidden" "" -> ".hidden" +posix join "a.." "x" -> "a../x" +posix join "a.." ".." -> "." +posix join "a.." "/y" -> "a../y" +posix join "a.." "C:\\z" -> "a../C:\\z" +posix join "a.." "" -> "a.." +posix join "a.b.c" "x" -> "a.b.c/x" +posix join "a.b.c" ".." -> "." +posix join "a.b.c" "/y" -> "a.b.c/y" +posix join "a.b.c" "C:\\z" -> "a.b.c/C:\\z" +posix join "a.b.c" "" -> "a.b.c" +posix join "C:" "x" -> "C:/x" +posix join "C:" ".." -> "." +posix join "C:" "/y" -> "C:/y" +posix join "C:" "C:\\z" -> "C:/C:\\z" +posix join "C:" "" -> "C:" +posix join "C:\\" "x" -> "C:\\/x" +posix join "C:\\" ".." -> "." +posix join "C:\\" "/y" -> "C:\\/y" +posix join "C:\\" "C:\\z" -> "C:\\/C:\\z" +posix join "C:\\" "" -> "C:\\" +posix join "C:x" "x" -> "C:x/x" +posix join "C:x" ".." -> "." +posix join "C:x" "/y" -> "C:x/y" +posix join "C:x" "C:\\z" -> "C:x/C:\\z" +posix join "C:x" "" -> "C:x" +posix join "\\" "x" -> "\\/x" +posix join "\\" ".." -> "." +posix join "\\" "/y" -> "\\/y" +posix join "\\" "C:\\z" -> "\\/C:\\z" +posix join "\\" "" -> "\\" +posix join "\\\\" "x" -> "\\\\/x" +posix join "\\\\" ".." -> "." +posix join "\\\\" "/y" -> "\\\\/y" +posix join "\\\\" "C:\\z" -> "\\\\/C:\\z" +posix join "\\\\" "" -> "\\\\" +posix join "/" "x" -> "/x" +posix join "/" ".." -> "/" +posix join "/" "/y" -> "/y" +posix join "/" "C:\\z" -> "/C:\\z" +posix join "/" "" -> "/" +posix join "//" "x" -> "/x" +posix join "//" ".." -> "/" +posix join "//" "/y" -> "/y" +posix join "//" "C:\\z" -> "/C:\\z" +posix join "//" "" -> "/" +posix join "\\\\srv\\share" "x" -> "\\\\srv\\share/x" +posix join "\\\\srv\\share" ".." -> "." +posix join "\\\\srv\\share" "/y" -> "\\\\srv\\share/y" +posix join "\\\\srv\\share" "C:\\z" -> "\\\\srv\\share/C:\\z" +posix join "\\\\srv\\share" "" -> "\\\\srv\\share" +posix join "\\\\srv\\share\\d" "x" -> "\\\\srv\\share\\d/x" +posix join "\\\\srv\\share\\d" ".." -> "." +posix join "\\\\srv\\share\\d" "/y" -> "\\\\srv\\share\\d/y" +posix join "\\\\srv\\share\\d" "C:\\z" -> "\\\\srv\\share\\d/C:\\z" +posix join "\\\\srv\\share\\d" "" -> "\\\\srv\\share\\d" +posix join "/usr/local" "x" -> "/usr/local/x" +posix join "/usr/local" ".." -> "/usr" +posix join "/usr/local" "/y" -> "/usr/local/y" +posix join "/usr/local" "C:\\z" -> "/usr/local/C:\\z" +posix join "/usr/local" "" -> "/usr/local" +posix join "usr" "x" -> "usr/x" +posix join "usr" ".." -> "." +posix join "usr" "/y" -> "usr/y" +posix join "usr" "C:\\z" -> "usr/C:\\z" +posix join "usr" "" -> "usr" +posix join "a/b" "x" -> "a/b/x" +posix join "a/b" ".." -> "a" +posix join "a/b" "/y" -> "a/b/y" +posix join "a/b" "C:\\z" -> "a/b/C:\\z" +posix join "a/b" "" -> "a/b" +posix join "a\\b" "x" -> "a\\b/x" +posix join "a\\b" ".." -> "." +posix join "a\\b" "/y" -> "a\\b/y" +posix join "a\\b" "C:\\z" -> "a\\b/C:\\z" +posix join "a\\b" "" -> "a\\b" +posix join "a//b" "x" -> "a/b/x" +posix join "a//b" ".." -> "a" +posix join "a//b" "/y" -> "a/b/y" +posix join "a//b" "C:\\z" -> "a/b/C:\\z" +posix join "a//b" "" -> "a/b" +posix join "a\\\\b" "x" -> "a\\\\b/x" +posix join "a\\\\b" ".." -> "." +posix join "a\\\\b" "/y" -> "a\\\\b/y" +posix join "a\\\\b" "C:\\z" -> "a\\\\b/C:\\z" +posix join "a\\\\b" "" -> "a\\\\b" +posix join " " "x" -> " /x" +posix join " " ".." -> "." +posix join " " "/y" -> " /y" +posix join " " "C:\\z" -> " /C:\\z" +posix join " " "" -> " " +posix join "a b" "x" -> "a b/x" +posix join "a b" ".." -> "." +posix join "a b" "/y" -> "a b/y" +posix join "a b" "C:\\z" -> "a b/C:\\z" +posix join "a b" "" -> "a b" +posix join "./x" "x" -> "x/x" +posix join "./x" ".." -> "." +posix join "./x" "/y" -> "x/y" +posix join "./x" "C:\\z" -> "x/C:\\z" +posix join "./x" "" -> "x" +posix join "../y" "x" -> "../y/x" +posix join "../y" ".." -> ".." +posix join "../y" "/y" -> "../y/y" +posix join "../y" "C:\\z" -> "../y/C:\\z" +posix join "../y" "" -> "../y" +posix join "/a/../b" "x" -> "/b/x" +posix join "/a/../b" ".." -> "/" +posix join "/a/../b" "/y" -> "/b/y" +posix join "/a/../b" "C:\\z" -> "/b/C:\\z" +posix join "/a/../b" "" -> "/b" +posix join "C:\\a\\..\\b" "x" -> "C:\\a\\..\\b/x" +posix join "C:\\a\\..\\b" ".." -> "." +posix join "C:\\a\\..\\b" "/y" -> "C:\\a\\..\\b/y" +posix join "C:\\a\\..\\b" "C:\\z" -> "C:\\a\\..\\b/C:\\z" +posix join "C:\\a\\..\\b" "" -> "C:\\a\\..\\b" +posix join "x/" "x" -> "x/x" +posix join "x/" ".." -> "." +posix join "x/" "/y" -> "x/y" +posix join "x/" "C:\\z" -> "x/C:\\z" +posix join "x/" "" -> "x/" +posix join "x\\" "x" -> "x\\/x" +posix join "x\\" ".." -> "." +posix join "x\\" "/y" -> "x\\/y" +posix join "x\\" "C:\\z" -> "x\\/C:\\z" +posix join "x\\" "" -> "x\\" +posix relative "/a/b" "/a/b" -> "" +posix relative "/a/b" "/a" -> ".." +posix relative "/a/b" "/" -> "../.." +posix relative "/a/b" "/a/b/c" -> "c" +posix relative "/a/b" "/x/y" -> "../../x/y" +posix relative "/a" "/a/b" -> "b" +posix relative "/a" "/a" -> "" +posix relative "/a" "/" -> ".." +posix relative "/a" "/a/b/c" -> "b/c" +posix relative "/a" "/x/y" -> "../x/y" +posix relative "/" "/a/b" -> "a/b" +posix relative "/" "/a" -> "a" +posix relative "/" "/" -> "" +posix relative "/" "/a/b/c" -> "a/b/c" +posix relative "/" "/x/y" -> "x/y" +posix relative "/a/b/c" "/a/b" -> ".." +posix relative "/a/b/c" "/a" -> "../.." +posix relative "/a/b/c" "/" -> "../../.." +posix relative "/a/b/c" "/a/b/c" -> "" +posix relative "/a/b/c" "/x/y" -> "../../../x/y" +posix relative "/x/y" "/a/b" -> "../../a/b" +posix relative "/x/y" "/a" -> "../../a" +posix relative "/x/y" "/" -> "../.." +posix relative "/x/y" "/a/b/c" -> "../../a/b/c" +posix relative "/x/y" "/x/y" -> "" +posix sep "/" delimiter ":" +win32 "" n="." d="." b="" e="" a=false +win32 "." n="." d="." b="." e="" a=false +win32 ".." n=".." d="." b=".." e="" a=false +win32 "..." n="..." d="." b="..." e="." a=false +win32 "a" n="a" d="." b="a" e="" a=false +win32 "a.txt" n="a.txt" d="." b="a.txt" e=".txt" a=false +win32 ".hidden" n=".hidden" d="." b=".hidden" e="" a=false +win32 "a.." n="a.." d="." b="a.." e="." a=false +win32 "a.b.c" n="a.b.c" d="." b="a.b.c" e=".c" a=false +win32 "C:" n="C:." d="C:" b="" e="" a=false +win32 "C:\\" n="C:\\" d="C:\\" b="" e="" a=true +win32 "C:x" n="C:x" d="C:" b="x" e="" a=false +win32 "\\" n="\\" d="\\" b="" e="" a=true +win32 "\\\\" n="\\" d="\\" b="" e="" a=true +win32 "/" n="\\" d="/" b="" e="" a=true +win32 "//" n="\\" d="/" b="" e="" a=true +win32 "\\\\srv\\share" n="\\\\srv\\share\\" d="\\\\srv\\share" b="share" e="" a=true +win32 "\\\\srv\\share\\d" n="\\\\srv\\share\\d" d="\\\\srv\\share\\" b="d" e="" a=true +win32 "/usr/local" n="\\usr\\local" d="/usr" b="local" e="" a=true +win32 "usr" n="usr" d="." b="usr" e="" a=false +win32 "a/b" n="a\\b" d="a" b="b" e="" a=false +win32 "a\\b" n="a\\b" d="a" b="b" e="" a=false +win32 "a//b" n="a\\b" d="a/" b="b" e="" a=false +win32 "a\\\\b" n="a\\b" d="a\\" b="b" e="" a=false +win32 " " n=" " d="." b=" " e="" a=false +win32 "a b" n="a b" d="." b="a b" e="" a=false +win32 "./x" n="x" d="." b="x" e="" a=false +win32 "../y" n="..\\y" d=".." b="y" e="" a=false +win32 "/a/../b" n="\\b" d="/a/.." b="b" e="" a=true +win32 "C:\\a\\..\\b" n="C:\\b" d="C:\\a\\.." b="b" e="" a=true +win32 "x/" n="x\\" d="." b="x" e="" a=false +win32 "x\\" n="x\\" d="." b="x" e="" a=false +win32 join "" "x" -> "x" +win32 join "" ".." -> ".." +win32 join "" "/y" -> "\\y" +win32 join "" "C:\\z" -> "C:\\z" +win32 join "" "" -> "." +win32 join "." "x" -> "x" +win32 join "." ".." -> ".." +win32 join "." "/y" -> "y" +win32 join "." "C:\\z" -> ".\\C:\\z" +win32 join "." "" -> "." +win32 join ".." "x" -> "..\\x" +win32 join ".." ".." -> "..\\.." +win32 join ".." "/y" -> "..\\y" +win32 join ".." "C:\\z" -> ".\\..\\C:\\z" +win32 join ".." "" -> ".." +win32 join "..." "x" -> "...\\x" +win32 join "..." ".." -> "." +win32 join "..." "/y" -> "...\\y" +win32 join "..." "C:\\z" -> ".\\...\\C:\\z" +win32 join "..." "" -> "..." +win32 join "a" "x" -> "a\\x" +win32 join "a" ".." -> "." +win32 join "a" "/y" -> "a\\y" +win32 join "a" "C:\\z" -> ".\\a\\C:\\z" +win32 join "a" "" -> "a" +win32 join "a.txt" "x" -> "a.txt\\x" +win32 join "a.txt" ".." -> "." +win32 join "a.txt" "/y" -> "a.txt\\y" +win32 join "a.txt" "C:\\z" -> ".\\a.txt\\C:\\z" +win32 join "a.txt" "" -> "a.txt" +win32 join ".hidden" "x" -> ".hidden\\x" +win32 join ".hidden" ".." -> "." +win32 join ".hidden" "/y" -> ".hidden\\y" +win32 join ".hidden" "C:\\z" -> ".\\.hidden\\C:\\z" +win32 join ".hidden" "" -> ".hidden" +win32 join "a.." "x" -> "a..\\x" +win32 join "a.." ".." -> "." +win32 join "a.." "/y" -> "a..\\y" +win32 join "a.." "C:\\z" -> ".\\a..\\C:\\z" +win32 join "a.." "" -> "a.." +win32 join "a.b.c" "x" -> "a.b.c\\x" +win32 join "a.b.c" ".." -> "." +win32 join "a.b.c" "/y" -> "a.b.c\\y" +win32 join "a.b.c" "C:\\z" -> ".\\a.b.c\\C:\\z" +win32 join "a.b.c" "" -> "a.b.c" +win32 join "C:" "x" -> "C:\\x" +win32 join "C:" ".." -> "C:\\" +win32 join "C:" "/y" -> "C:\\y" +win32 join "C:" "C:\\z" -> "C:\\C:\\z" +win32 join "C:" "" -> "C:." +win32 join "C:\\" "x" -> "C:\\x" +win32 join "C:\\" ".." -> "C:\\" +win32 join "C:\\" "/y" -> "C:\\y" +win32 join "C:\\" "C:\\z" -> "C:\\C:\\z" +win32 join "C:\\" "" -> "C:\\" +win32 join "C:x" "x" -> "C:x\\x" +win32 join "C:x" ".." -> "C:." +win32 join "C:x" "/y" -> "C:x\\y" +win32 join "C:x" "C:\\z" -> "C:x\\C:\\z" +win32 join "C:x" "" -> "C:x" +win32 join "\\" "x" -> "\\x" +win32 join "\\" ".." -> "\\" +win32 join "\\" "/y" -> "\\y" +win32 join "\\" "C:\\z" -> "\\C:\\z" +win32 join "\\" "" -> "\\" +win32 join "\\\\" "x" -> "\\x" +win32 join "\\\\" ".." -> "\\" +win32 join "\\\\" "/y" -> "\\y" +win32 join "\\\\" "C:\\z" -> "\\C:\\z" +win32 join "\\\\" "" -> "\\" +win32 join "/" "x" -> "\\x" +win32 join "/" ".." -> "\\" +win32 join "/" "/y" -> "\\y" +win32 join "/" "C:\\z" -> "\\C:\\z" +win32 join "/" "" -> "\\" +win32 join "//" "x" -> "\\x" +win32 join "//" ".." -> "\\" +win32 join "//" "/y" -> "\\y" +win32 join "//" "C:\\z" -> "\\C:\\z" +win32 join "//" "" -> "\\" +win32 join "\\\\srv\\share" "x" -> "\\\\srv\\share\\x" +win32 join "\\\\srv\\share" ".." -> "\\\\srv\\share\\" +win32 join "\\\\srv\\share" "/y" -> "\\\\srv\\share\\y" +win32 join "\\\\srv\\share" "C:\\z" -> "\\\\srv\\share\\C:\\z" +win32 join "\\\\srv\\share" "" -> "\\\\srv\\share\\" +win32 join "\\\\srv\\share\\d" "x" -> "\\\\srv\\share\\d\\x" +win32 join "\\\\srv\\share\\d" ".." -> "\\\\srv\\share\\" +win32 join "\\\\srv\\share\\d" "/y" -> "\\\\srv\\share\\d\\y" +win32 join "\\\\srv\\share\\d" "C:\\z" -> "\\\\srv\\share\\d\\C:\\z" +win32 join "\\\\srv\\share\\d" "" -> "\\\\srv\\share\\d" +win32 join "/usr/local" "x" -> "\\usr\\local\\x" +win32 join "/usr/local" ".." -> "\\usr" +win32 join "/usr/local" "/y" -> "\\usr\\local\\y" +win32 join "/usr/local" "C:\\z" -> "\\usr\\local\\C:\\z" +win32 join "/usr/local" "" -> "\\usr\\local" +win32 join "usr" "x" -> "usr\\x" +win32 join "usr" ".." -> "." +win32 join "usr" "/y" -> "usr\\y" +win32 join "usr" "C:\\z" -> ".\\usr\\C:\\z" +win32 join "usr" "" -> "usr" +win32 join "a/b" "x" -> "a\\b\\x" +win32 join "a/b" ".." -> "a" +win32 join "a/b" "/y" -> "a\\b\\y" +win32 join "a/b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join "a/b" "" -> "a\\b" +win32 join "a\\b" "x" -> "a\\b\\x" +win32 join "a\\b" ".." -> "a" +win32 join "a\\b" "/y" -> "a\\b\\y" +win32 join "a\\b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join "a\\b" "" -> "a\\b" +win32 join "a//b" "x" -> "a\\b\\x" +win32 join "a//b" ".." -> "a" +win32 join "a//b" "/y" -> "a\\b\\y" +win32 join "a//b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join "a//b" "" -> "a\\b" +win32 join "a\\\\b" "x" -> "a\\b\\x" +win32 join "a\\\\b" ".." -> "a" +win32 join "a\\\\b" "/y" -> "a\\b\\y" +win32 join "a\\\\b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join "a\\\\b" "" -> "a\\b" +win32 join " " "x" -> " \\x" +win32 join " " ".." -> "." +win32 join " " "/y" -> " \\y" +win32 join " " "C:\\z" -> ".\\ \\C:\\z" +win32 join " " "" -> " " +win32 join "a b" "x" -> "a b\\x" +win32 join "a b" ".." -> "." +win32 join "a b" "/y" -> "a b\\y" +win32 join "a b" "C:\\z" -> ".\\a b\\C:\\z" +win32 join "a b" "" -> "a b" +win32 join "./x" "x" -> "x\\x" +win32 join "./x" ".." -> "." +win32 join "./x" "/y" -> "x\\y" +win32 join "./x" "C:\\z" -> ".\\x\\C:\\z" +win32 join "./x" "" -> "x" +win32 join "../y" "x" -> "..\\y\\x" +win32 join "../y" ".." -> ".." +win32 join "../y" "/y" -> "..\\y\\y" +win32 join "../y" "C:\\z" -> ".\\..\\y\\C:\\z" +win32 join "../y" "" -> "..\\y" +win32 join "/a/../b" "x" -> "\\b\\x" +win32 join "/a/../b" ".." -> "\\" +win32 join "/a/../b" "/y" -> "\\b\\y" +win32 join "/a/../b" "C:\\z" -> "\\b\\C:\\z" +win32 join "/a/../b" "" -> "\\b" +win32 join "C:\\a\\..\\b" "x" -> "C:\\b\\x" +win32 join "C:\\a\\..\\b" ".." -> "C:\\" +win32 join "C:\\a\\..\\b" "/y" -> "C:\\b\\y" +win32 join "C:\\a\\..\\b" "C:\\z" -> "C:\\b\\C:\\z" +win32 join "C:\\a\\..\\b" "" -> "C:\\b" +win32 join "x/" "x" -> "x\\x" +win32 join "x/" ".." -> "." +win32 join "x/" "/y" -> "x\\y" +win32 join "x/" "C:\\z" -> ".\\x\\C:\\z" +win32 join "x/" "" -> "x\\" +win32 join "x\\" "x" -> "x\\x" +win32 join "x\\" ".." -> "." +win32 join "x\\" "/y" -> "x\\y" +win32 join "x\\" "C:\\z" -> ".\\x\\C:\\z" +win32 join "x\\" "" -> "x\\" +win32 relative "C:\\a\\b" "C:\\a\\b" -> "" +win32 relative "C:\\a\\b" "C:\\a" -> ".." +win32 relative "C:\\a\\b" "C:\\" -> "..\\.." +win32 relative "C:\\a\\b" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "C:\\a\\b" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" +win32 relative "C:\\a\\b" "D:\\a" -> "D:\\a" +win32 relative "C:\\a" "C:\\a\\b" -> "b" +win32 relative "C:\\a" "C:\\a" -> "" +win32 relative "C:\\a" "C:\\" -> ".." +win32 relative "C:\\a" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "C:\\a" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" +win32 relative "C:\\a" "D:\\a" -> "D:\\a" +win32 relative "C:\\" "C:\\a\\b" -> "a\\b" +win32 relative "C:\\" "C:\\a" -> "a" +win32 relative "C:\\" "C:\\" -> "" +win32 relative "C:\\" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "C:\\" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" +win32 relative "C:\\" "D:\\a" -> "D:\\a" +win32 relative "\\\\srv\\share\\a" "C:\\a\\b" -> "C:\\a\\b" +win32 relative "\\\\srv\\share\\a" "C:\\a" -> "C:\\a" +win32 relative "\\\\srv\\share\\a" "C:\\" -> "C:\\" +win32 relative "\\\\srv\\share\\a" "\\\\srv\\share\\a" -> "" +win32 relative "\\\\srv\\share\\a" "\\\\srv\\share\\b" -> "..\\b" +win32 relative "\\\\srv\\share\\a" "D:\\a" -> "D:\\a" +win32 relative "\\\\srv\\share\\b" "C:\\a\\b" -> "C:\\a\\b" +win32 relative "\\\\srv\\share\\b" "C:\\a" -> "C:\\a" +win32 relative "\\\\srv\\share\\b" "C:\\" -> "C:\\" +win32 relative "\\\\srv\\share\\b" "\\\\srv\\share\\a" -> "..\\a" +win32 relative "\\\\srv\\share\\b" "\\\\srv\\share\\b" -> "" +win32 relative "\\\\srv\\share\\b" "D:\\a" -> "D:\\a" +win32 relative "D:\\a" "C:\\a\\b" -> "C:\\a\\b" +win32 relative "D:\\a" "C:\\a" -> "C:\\a" +win32 relative "D:\\a" "C:\\" -> "C:\\" +win32 relative "D:\\a" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "D:\\a" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" +win32 relative "D:\\a" "D:\\a" -> "" +win32 sep "\\" delimiter ";" +cases: 445 diff --git a/tests/fixtures/node_path.known b/tests/fixtures/node_path.known new file mode 100644 index 0000000..2860d40 --- /dev/null +++ b/tests/fixtures/node_path.known @@ -0,0 +1,55 @@ +# Lines Node prints that this runtime's win32 port does not match. +# All Windows-only edge cases: a drive-relative path with an empty +# tail, a relative path across two roots, a segment that looks like +# a device inside a relative path. posix has no entries here and +# must not gain any. +win32 "C:" n="C:." d="C:" b="" e="" a=false +win32 "\\" n="\\" d="\\" b="" e="" a=true +win32 "/" n="\\" d="/" b="" e="" a=true +win32 join "." "C:\\z" -> ".\\C:\\z" +win32 join ".." "C:\\z" -> ".\\..\\C:\\z" +win32 join "..." "C:\\z" -> ".\\...\\C:\\z" +win32 join "a" "C:\\z" -> ".\\a\\C:\\z" +win32 join "a.txt" "C:\\z" -> ".\\a.txt\\C:\\z" +win32 join ".hidden" "C:\\z" -> ".\\.hidden\\C:\\z" +win32 join "a.." "C:\\z" -> ".\\a..\\C:\\z" +win32 join "a.b.c" "C:\\z" -> ".\\a.b.c\\C:\\z" +win32 join "C:" "" -> "C:." +win32 join "C:x" ".." -> "C:." +win32 join "\\" "C:\\z" -> "\\C:\\z" +win32 join "\\\\" "C:\\z" -> "\\C:\\z" +win32 join "/" "C:\\z" -> "\\C:\\z" +win32 join "//" "C:\\z" -> "\\C:\\z" +win32 join "usr" "C:\\z" -> ".\\usr\\C:\\z" +win32 join "a/b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join "a\\b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join "a//b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join "a\\\\b" "C:\\z" -> ".\\a\\b\\C:\\z" +win32 join " " "C:\\z" -> ".\\ \\C:\\z" +win32 join "a b" "C:\\z" -> ".\\a b\\C:\\z" +win32 join "./x" "C:\\z" -> ".\\x\\C:\\z" +win32 join "../y" "C:\\z" -> ".\\..\\y\\C:\\z" +win32 join "x/" "C:\\z" -> ".\\x\\C:\\z" +win32 join "x\\" "C:\\z" -> ".\\x\\C:\\z" +win32 relative "C:\\a\\b" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "C:\\a\\b" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" +win32 relative "C:\\a\\b" "D:\\a" -> "D:\\a" +win32 relative "C:\\a" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "C:\\a" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" +win32 relative "C:\\a" "D:\\a" -> "D:\\a" +win32 relative "C:\\" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "C:\\" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" +win32 relative "C:\\" "D:\\a" -> "D:\\a" +win32 relative "\\\\srv\\share\\a" "C:\\a\\b" -> "C:\\a\\b" +win32 relative "\\\\srv\\share\\a" "C:\\a" -> "C:\\a" +win32 relative "\\\\srv\\share\\a" "C:\\" -> "C:\\" +win32 relative "\\\\srv\\share\\a" "D:\\a" -> "D:\\a" +win32 relative "\\\\srv\\share\\b" "C:\\a\\b" -> "C:\\a\\b" +win32 relative "\\\\srv\\share\\b" "C:\\a" -> "C:\\a" +win32 relative "\\\\srv\\share\\b" "C:\\" -> "C:\\" +win32 relative "\\\\srv\\share\\b" "D:\\a" -> "D:\\a" +win32 relative "D:\\a" "C:\\a\\b" -> "C:\\a\\b" +win32 relative "D:\\a" "C:\\a" -> "C:\\a" +win32 relative "D:\\a" "C:\\" -> "C:\\" +win32 relative "D:\\a" "\\\\srv\\share\\a" -> "\\\\srv\\share\\a" +win32 relative "D:\\a" "\\\\srv\\share\\b" -> "\\\\srv\\share\\b" diff --git a/tests/fixtures/node_path.mjs b/tests/fixtures/node_path.mjs new file mode 100644 index 0000000..0a36e73 --- /dev/null +++ b/tests/fixtures/node_path.mjs @@ -0,0 +1,64 @@ +// Every path function, posix and win32, over a wide corpus, against what +// Node prints for the same corpus (node_path.expected, recorded from Node). +// +// posix has to match exactly -- it is what this runtime runs on. win32 is a +// port of the same algorithms and agrees with Node on the ordinary cases; +// where it does not, the difference is listed in node_path.known so it is +// visible rather than silent, and a NEW difference fails the test. +// +// To refresh after an intentional change: +// node tests/fixtures/node_path.mjs > tests/fixtures/node_path.expected +import path from "node:path"; +import { readFileSync } from "node:fs"; + +const printed = []; +const console = { log: (...args) => printed.push(args.join(" ")) }; +const pieces = ["", ".", "..", "...", "a", "a.txt", ".hidden", "a..", "a.b.c", "C:", "C:\\", "C:x", + "\\", "\\\\", "/", "//", "\\\\srv\\share", "\\\\srv\\share\\d", "/usr/local", "usr", "a/b", "a\\b", + "a//b", "a\\\\b", " ", "a b", "./x", "../y", "/a/../b", "C:\\a\\..\\b", "x/", "x\\"]; +let out = 0; +for (const impl of ["posix", "win32"]) { + const p = path[impl]; + for (const one of pieces) { + console.log(impl, JSON.stringify(one), + "n=" + JSON.stringify(p.normalize(one)), + "d=" + JSON.stringify(p.dirname(one)), + "b=" + JSON.stringify(p.basename(one)), + "e=" + JSON.stringify(p.extname(one)), + "a=" + p.isAbsolute(one)); + out++; + } + for (const a of pieces) for (const b of ["x", "..", "/y", "C:\\z", ""]) { + console.log(impl, "join", JSON.stringify(a), JSON.stringify(b), "->", JSON.stringify(p.join(a, b))); + out++; + } + // Absolute on both sides only, and absolute in the sense this half of the + // module means: anything relative is resolved against the working + // directory, and the answer would then depend on where this ran. + const roots = impl === "posix" + ? ["/a/b", "/a", "/", "/a/b/c", "/x/y"] + : ["C:\\a\\b", "C:\\a", "C:\\", "\\\\srv\\share\\a", "\\\\srv\\share\\b", "D:\\a"]; + for (const a of roots) for (const b of roots) { + console.log(impl, "relative", JSON.stringify(a), JSON.stringify(b), "->", JSON.stringify(p.relative(a, b))); + out++; + } + console.log(impl, "sep", JSON.stringify(p.sep), "delimiter", JSON.stringify(p.delimiter)); +} +console.log("cases:", out); + +const here = (name) => new URL("./" + name, import.meta.url).pathname; +const expected = readFileSync(here("node_path.expected"), "utf8").trimEnd().split("\n"); +const known = new Set(readFileSync(here("node_path.known"), "utf8").split("\n").filter(l => l && !l.startsWith("#"))); + +let bad = 0, allowed = 0; +for (let i = 0; i < Math.max(printed.length, expected.length); i++) { + if (printed[i] === expected[i]) continue; + if (known.has(expected[i])) { allowed++; continue; } + bad++; + globalThis.console.log("FAIL want " + (expected[i] ?? "(nothing)")); + globalThis.console.log(" got " + (printed[i] ?? "(nothing)")); +} +globalThis.console.log(bad === 0 + ? `node:path: ${printed.length - allowed} of ${printed.length} identical to Node, ${allowed} known win32 differences` + : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_process_cwd.mjs b/tests/fixtures/node_process_cwd.mjs new file mode 100644 index 0000000..4d2e076 --- /dev/null +++ b/tests/fixtures/node_process_cwd.mjs @@ -0,0 +1,28 @@ +// process.cwd() is cached, so process.chdir() has to be the thing that +// clears it -- and a chdir that fails must not. +import path from "node:path"; +import os from "node:os"; +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + n + " got=" + got + (ok ? "" : " want=" + want)); }; + +const start = process.cwd(); +check("absolute", path.isAbsolute(start), true); +check("stable", process.cwd(), start); + +// The temporary directory may be a symlink, so what cwd reports after +// moving there is compared with itself rather than with the name used. +process.chdir(os.tmpdir()); +const moved = process.cwd(); +check("chdir is seen", moved !== start, true); +check("still seen twice", process.cwd(), moved); + +let code = ""; +try { process.chdir(path.join(moved, "no-such-directory-here")); } catch (e) { code = e.code; } +check("a failed chdir throws", code, "ENOENT"); +check("a failed chdir changes nothing", process.cwd(), moved); + +process.chdir(start); +check("back where we started", process.cwd(), start); +console.log(bad === 0 ? "process.cwd: cache follows chdir" : "FAILURES: " + bad); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_querystring.expected b/tests/fixtures/node_querystring.expected new file mode 100644 index 0000000..11ff742 --- /dev/null +++ b/tests/fixtures/node_querystring.expected @@ -0,0 +1,33 @@ +"a=1&b=2" -> {"a":"1","b":"2"} +"a=1&a=2&a=3" -> {"a":["1","2","3"]} +"a" -> {"a":""} +"a=" -> {"a":""} +"=b" -> {"":"b"} +"" -> {} +"a=1&&b=2" -> {"a":"1","b":"2"} +"&&" -> {} +"name=a+b" -> {"name":"a b"} +"city=S%C3%A3o+Paulo" -> {"city":"São Paulo"} +"bad=%zz" -> {"bad":"%zz"} +"half=%" -> {"half":"%"} +"p=%2Fslash" -> {"p":"/slash"} +"x=1;y=2" -> {"x":"1;y=2"} +"k%5B%5D=1&k%5B%5D=2" -> {"k[]":["1","2"]} +"utf=%F0%9F%8E%89" -> {"utf":"🎉"} +"eq=a=b" -> {"eq":"a=b"} +"sp=a%20b" -> {"sp":"a b"} +"plus=a%2Bb" -> {"plus":"a+b"} +"empty=&next=1" -> {"empty":"","next":"1"} +"dup=1&dup=2&other=3" -> {"dup":["1","2"],"other":"3"} +custom sep -> {"a":"1","b":"2"} +maxKeys 2 -> {"a":"1","b":"2"} +stringify {"a":1,"b":"two"} -> "a=1&b=two" +stringify {"a":["1","2"]} -> "a=1&a=2" +stringify {"a b":"c d"} -> "a%20b=c%20d" +stringify {"e":""} -> "e=" +stringify {"n":null,"t":true,"f":false,"num":4.5} -> "n=&u=&t=true&f=false&num=4.5" +stringify {"é":"🎉"} -> "%C3%A9=%F0%9F%8E%89" +stringify {} -> "" +stringify custom -> "a:1|b:2" +escape -> a%20b%2Fc%3Fd%3De%26f%2Bg'()!~*. +unescape -> a b/c?d diff --git a/tests/fixtures/node_querystring.mjs b/tests/fixtures/node_querystring.mjs new file mode 100644 index 0000000..bc7a800 --- /dev/null +++ b/tests/fixtures/node_querystring.mjs @@ -0,0 +1,40 @@ +// node:querystring, which is native C (js_qs_* in src/node.c). Every line +// below is printed by Node too, and node_querystring.expected holds what it +// printed, so this runs without Node and fails on any divergence. +// +// To refresh after an intentional change: +// node tests/fixtures/node_querystring.mjs > tests/fixtures/node_querystring.expected +import qs from "node:querystring"; +import { readFileSync } from "node:fs"; + +const lines = []; +const console = { log: (...args) => lines.push(args.join(" ")) }; +const show = (v) => JSON.stringify(v, Object.keys(v ?? {}).sort()); +const cases = [ + "a=1&b=2", "a=1&a=2&a=3", "a", "a=", "=b", "", "a=1&&b=2", "&&", + "name=a+b", "city=S%C3%A3o+Paulo", "bad=%zz", "half=%", "p=%2Fslash", + "x=1;y=2", "k%5B%5D=1&k%5B%5D=2", "utf=%F0%9F%8E%89", "eq=a=b", + "sp=a%20b", "plus=a%2Bb", "empty=&next=1", "dup=1&dup=2&other=3", +]; +for (const c of cases) console.log(JSON.stringify(c), "->", show(qs.parse(c))); +console.log("custom sep ->", show(qs.parse("a:1|b:2", "|", ":"))); +console.log("maxKeys 2 ->", show(qs.parse("a=1&b=2&c=3", "&", "=", { maxKeys: 2 }))); +const objs = [ + { a: 1, b: "two" }, { a: ["1", "2"] }, { "a b": "c d" }, { e: "" }, + { n: null, u: undefined, t: true, f: false, num: 4.5 }, { "é": "🎉" }, {}, +]; +for (const o of objs) console.log("stringify", JSON.stringify(o), "->", JSON.stringify(qs.stringify(o))); +console.log("stringify custom ->", JSON.stringify(qs.stringify({ a: 1, b: 2 }, "|", ":"))); +console.log("escape ->", qs.escape("a b/c?d=e&f+g'()!~*.")); +console.log("unescape ->", qs.unescape("a%20b%2Fc%3Fd")); + +const expected = readFileSync(new URL("./node_querystring.expected", import.meta.url).pathname, "utf8").trimEnd().split("\n"); +let bad = 0; +for (let i = 0; i < Math.max(lines.length, expected.length); i++) { + if (lines[i] === expected[i]) continue; + bad++; + globalThis.console.log("FAIL want " + (expected[i] ?? "(nothing)")); + globalThis.console.log(" got " + (lines[i] ?? "(nothing)")); +} +globalThis.console.log(bad === 0 ? `node:querystring: ${lines.length} answers identical to Node` : `FAILURES: ${bad}`); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_stream.mjs b/tests/fixtures/node_stream.mjs index a2139e1..f84db9f 100644 --- a/tests/fixtures/node_stream.mjs +++ b/tests/fixtures/node_stream.mjs @@ -54,4 +54,46 @@ p("fromWeb", await collect(Readable.fromWeb( // Outside objectMode a stream refuses a non-byte chunk, as Node does. p("rejects raw number", (() => { try { new Writable({ write(c,e,cb){cb();} }).write(5); return "no"; } catch (e) { return e.code; } })()); +// The read queue: chunks leave through a cursor now, so a long queue has to +// come out in order, exactly once, and end when it is empty. +{ const r = new Readable({ objectMode: true, read(){} }); + for (let i = 0; i < 500; i++) r.push({ i }); + r.push(null); + const seen = await collect(r); + p("long queue length", seen.length); + p("long queue in order", seen.every((v, i) => v.i === i)); + p("long queue read after end", r.read()); } +{ const r = new Readable({ objectMode: true, read(){} }); + r.push("a"); r.push("b"); + const first = r.read(); + r.push("c"); + p("interleaved push and read", [first, r.read(), r.read(), r.read()]); } +{ const r = new Readable({ read(){} }); + r.push("ab"); r.push("cd"); r.push(null); + p("bytes join into one read", r.read().toString()); } + +// pipe is native now: the data path, the end option, unpipe, and an error +// travelling to the destination. +{ const src = new Readable({ read(){} }); const seen = []; + const dest = new Writable({ write(c, e, cb){ seen.push(c.toString()); cb(); } }); + src.pipe(dest); src.push("a"); src.push("b"); src.push(null); + await new Promise((r) => setTimeout(r, 10)); + p("pipe carries chunks", seen); } +{ const src = new Readable({ read(){} }); let ended = false; + const dest = new Writable({ write(c, e, cb){ cb(); } }); + dest.on("finish", () => { ended = true; }); + src.pipe(dest, { end: false }); src.push("x"); src.push(null); + await new Promise((r) => setTimeout(r, 10)); + p("end: false leaves it open", ended); } +{ const src = new Readable({ read(){} }); const seen = []; + const dest = new Writable({ write(c, e, cb){ seen.push(c.toString()); cb(); } }); + src.pipe(dest); src.push("kept"); await new Promise((r) => setTimeout(r, 5)); + src.unpipe(dest); src.push("dropped"); await new Promise((r) => setTimeout(r, 5)); + p("unpipe stops it", seen); } +// An error on the source reaches the destination here, which Node does not +// do -- it destroys the pipe instead -- so it is not checked in this file. +{ const src = new Readable({ read(){} }); + const dest = new Writable({ write(c, e, cb){ cb(); } }); + p("pipe returns the destination", src.pipe(dest) === dest); } + console.log(L.join("\n")); diff --git a/tests/fixtures/node_string_decoder.mjs b/tests/fixtures/node_string_decoder.mjs new file mode 100644 index 0000000..0db9bc6 --- /dev/null +++ b/tests/fixtures/node_string_decoder.mjs @@ -0,0 +1,30 @@ +// StringDecoder's whole job is not splitting a character across a chunk +// boundary. utf-8 is native now, so every way of cutting a multi-byte +// character in half has to come out the same as Node's. +import { StringDecoder } from "node:string_decoder"; +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + n + " got=" + JSON.stringify(got) + (ok ? "" : " want=" + JSON.stringify(want))); }; + +const run = (chunks) => { + const d = new StringDecoder("utf8"); + let out = ""; + for (const c of chunks) out += d.write(Buffer.from(c)); + return out + d.end(); +}; +check("ascii", run([[0x61, 0x62]]), "ab"); +check("whole character", run([[0xe6, 0x97, 0xa5]]), "日"); +check("split after one byte", run([[0xe6], [0x97, 0xa5]]), "日"); +check("split after two", run([[0xe6, 0x97], [0xa5]]), "日"); +check("byte at a time", run([[0xe6], [0x97], [0xa5]]), "日"); +check("surrogate pair split", run([[0xf0, 0x9f], [0x8e, 0x89]]), "🎉"); +check("two byte then ascii", run([[0xc3], [0xa9, 0x21]]), "é!"); +check("stranded byte", run([[0xe6]]), "�"); +check("stranded pair", run([[0xf0, 0x9f]]), "\ufffd"); +check("nothing at all", run([[]]), ""); +check("text through unchanged", new StringDecoder("utf8").write("already text"), "already text"); +check("mixed chunks", run([[0x61], [0xe6, 0x97, 0xa5], [0x62]]), "a日b"); +check("end with a chunk", (() => { const d = new StringDecoder("utf8"); + d.write(Buffer.from([0xe6, 0x97])); return d.end(Buffer.from([0xa5])); })(), "日"); +console.log(bad === 0 ? "node:string_decoder: boundaries hold" : "FAILURES: " + bad); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/node_zlib_reuse.mjs b/tests/fixtures/node_zlib_reuse.mjs new file mode 100644 index 0000000..c92814d --- /dev/null +++ b/tests/fixtures/node_zlib_reuse.mjs @@ -0,0 +1,49 @@ +// The compressor and the decompressor are kept between calls and reset +// rather than rebuilt, so a run of calls has to keep giving the same bytes +// -- including when the window or the level changes between them, which is +// what makes the kept stream unusable and forces a fresh one. +import zlib from "node:zlib"; +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + n + " got=" + got + (ok ? "" : " want=" + want)); }; + +const text = "hello world ".repeat(20); +const bytes = Buffer.from(text); + +// Same settings, over and over: the second call is the one using a reset +// stream rather than a fresh one. +const first = zlib.gzipSync(bytes).toString("hex"); +for (let i = 0; i < 5; i++) check("gzip is stable " + i, zlib.gzipSync(bytes).toString("hex"), first); +for (let i = 0; i < 5; i++) check("round trip " + i, zlib.gunzipSync(zlib.gzipSync(bytes)).toString(), text); + +// Different containers around the same deflate data: each changes the window +// bits, so the kept stream cannot be reused for the next one. +check("deflate then gzip", zlib.gunzipSync(zlib.gzipSync(bytes)).toString(), text); +check("gzip then deflate", zlib.inflateSync(zlib.deflateSync(bytes)).toString(), text); +check("deflate then raw", zlib.inflateRawSync(zlib.deflateRawSync(bytes)).toString(), text); +check("raw then zlib", zlib.inflateSync(zlib.deflateSync(bytes)).toString(), text); + +// Levels, alternating: a kept stream carries its level, so a different one +// has to be noticed. +const cheap = zlib.gzipSync(bytes, { level: 1 }).toString("hex"); +const dear = zlib.gzipSync(bytes, { level: 9 }).toString("hex"); +check("levels differ", cheap === dear, false); +for (let i = 0; i < 3; i++) { + check("level 1 stable " + i, zlib.gzipSync(bytes, { level: 1 }).toString("hex"), cheap); + check("level 9 stable " + i, zlib.gzipSync(bytes, { level: 9 }).toString("hex"), dear); +} +check("level 1 round trip", zlib.gunzipSync(zlib.gzipSync(bytes, { level: 1 })).toString(), text); + +// Sizes either side of the output buffer's first guess, and empty input. +for (const size of [0, 1, 1023, 1024, 5000, 100000]) { + const payload = Buffer.from("a".repeat(size)); + check("size " + size, zlib.gunzipSync(zlib.gzipSync(payload)).length, size); +} +// A failed inflate must not leave a broken stream behind for the next call. +let threw = false; +try { zlib.gunzipSync(Buffer.from("not gzip data at all")); } catch { threw = true; } +check("bad input throws", threw, true); +check("and the next call still works", zlib.gunzipSync(zlib.gzipSync(bytes)).toString(), text); + +console.log(bad === 0 ? "node:zlib: reset streams give the same bytes" : "FAILURES: " + bad); +if (bad !== 0) process.exit(1); diff --git a/tests/fixtures/reject-mut-borrow.sx b/tests/fixtures/reject-mut-borrow.sx new file mode 100644 index 0000000..be0021b --- /dev/null +++ b/tests/fixtures/reject-mut-borrow.sx @@ -0,0 +1,12 @@ +// `&mut` is an exclusive borrow and requires a mutable owner +// (spec/LANGUAGE.md). `value` is a plain `let`, so this must not compile. +interface Counter { + hits: i32; +} + +function bump(c: &mut Counter): void { + c.hits += 1; +} + +let value: Counter = { hits: 0 }; +bump(&mut value); diff --git a/tests/fixtures/serve_bind.mjs b/tests/fixtures/serve_bind.mjs new file mode 100644 index 0000000..7f4756a --- /dev/null +++ b/tests/fixtures/serve_bind.mjs @@ -0,0 +1,39 @@ +// Where a server binds, and on what terms. The address used to be hardcoded +// to loopback with the option accepted and ignored, so a server could not be +// reached from another machine at all. +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok?"ok ":"FAIL ") + n + " got=" + JSON.stringify(got) + " want=" + JSON.stringify(want)); }; + +const loopback = Sxn.serve({ port: 0 }, () => new Response("loopback")); +check("loopback by default", loopback.hostname, "127.0.0.1"); +check("url names the host", loopback.url, `http://127.0.0.1:${loopback.port}`); +check("serves", await (await fetch(loopback.url)).text(), "loopback"); +loopback.stop(); + +const any = Sxn.serve({ port: 0, hostname: "0.0.0.0" }, () => new Response("any")); +check("binds where asked", any.hostname, "0.0.0.0"); +check("reachable there", await (await fetch(`http://127.0.0.1:${any.port}/`)).text(), "any"); +any.stop(); + +// Node calls it `host`; Bun and Deno call it `hostname`. Both work. +const named = Sxn.serve({ port: 0, host: "localhost" }, () => new Response("named")); +check("localhost resolves", named.hostname, "127.0.0.1"); +check("and serves", await (await fetch(named.url)).text(), "named"); +named.stop(); + +let threw = ""; +try { Sxn.serve({ port: 0, hostname: "nope.invalid" }, () => new Response("x")); } +catch (e) { threw = e.message; } +check("a name it cannot bind is an error", /is not an IP address/.test(threw), true); + +// reusePort is what lets several processes share a port and so use several +// cores. The kernels that distribute connections are Linux, the BSDs, +// Solaris and AIX; macOS says so rather than handing one process everything. +let reuse = ""; +try { const s = Sxn.serve({ port: 0, reusePort: true }, () => new Response("r")); s.stop(); reuse = "ok"; } +catch (e) { reuse = /not supported on this platform/.test(e.message) ? "unsupported" : e.message; } +check("reusePort works or says why", reuse === "ok" || reuse === "unsupported", true); + +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/serve_body_bytes.mjs b/tests/fixtures/serve_body_bytes.mjs new file mode 100644 index 0000000..3c80576 --- /dev/null +++ b/tests/fixtures/serve_body_bytes.mjs @@ -0,0 +1,34 @@ +// The request body is handed to JavaScript as the bytes the server already +// read, not copied into a string on the way. Everything that reads a body +// still has to work, and a pipelined request -- where the buffer cannot be +// handed over -- has to work too. +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok?"ok ":"FAIL ") + n + " got=" + JSON.stringify(got) + " want=" + JSON.stringify(want)); }; + +const server = Sxn.serve({ port: 0 }, async (req) => { + const url = new URL(req.url); + if (url.pathname === "/json") return Response.json({ n: (await req.json()).length }); + if (url.pathname === "/text") return new Response(await req.text()); + if (url.pathname === "/bytes") return Response.json({ bytes: (await req.arrayBuffer()).byteLength }); + if (url.pathname === "/twice") { const a = await req.text(); return new Response(String(a.length)); } + if (url.pathname === "/ignored") return new Response("never read it"); + return new Response("?", { status: 404 }); +}); +const post = (path, body, type) => fetch(server.url + path, { method: "POST", headers: { "content-type": type ?? "application/json" }, body }); + +const big = JSON.stringify(Array.from({ length: 5000 }, (_, i) => ({ i }))); +check("a big JSON body parses", (await (await post("/json", big)).json()).n, 5000); +check("and again on the same connection", (await (await post("/json", big)).json()).n, 5000); +check("text comes back whole", (await (await post("/text", "hello body", "text/plain")).text()), "hello body"); +check("non-ASCII survives", (await (await post("/text", "héllo 🎉", "text/plain")).text()), "héllo 🎉"); +check("bytes are the byte length", (await (await post("/bytes", "héllo", "text/plain")).json()).bytes, 6); +check("a body nobody reads", await (await post("/ignored", big)).text(), "never read it"); +check("an empty body", await (await post("/text", "", "text/plain")).text(), ""); +let threw = false; +try { await (await post("/json", "not json")).json(); } catch { threw = true; } +check("bad JSON is an error", threw, true); + +server.stop(); +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/serve_fetch_shape.mjs b/tests/fixtures/serve_fetch_shape.mjs index 421b2e7..dd6a99e 100644 --- a/tests/fixtures/serve_fetch_shape.mjs +++ b/tests/fixtures/serve_fetch_shape.mjs @@ -1,5 +1,5 @@ // Sxn.serve in Request/Response terms -- the shape spec/RUNTIME.md documents -// and the one a handler written for any other WinterCG runtime already uses. +// and the one a handler written for any other WinterTC runtime already uses. // Regression for three things that each broke the documented example: a // returned Response wrote a garbled reply, `new URL(req.url)` threw because // req.url was a bare path, and `port: 0` reported 0 instead of the port the diff --git a/tests/fixtures/serve_keepalive.mjs b/tests/fixtures/serve_keepalive.mjs new file mode 100644 index 0000000..dfe9996 --- /dev/null +++ b/tests/fixtures/serve_keepalive.mjs @@ -0,0 +1,28 @@ +// A connection survives its response. Until it did, every request paid for a +// new TCP connection: the server answered `Connection: close` and hung up, so +// a client doing more than a handful of requests ran out of ephemeral ports +// before the server ran out of capacity. +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok?"ok ":"FAIL ") + n + " got=" + JSON.stringify(got) + " want=" + JSON.stringify(want)); }; + +const server = Sxn.serve({ port: 0 }, () => new Response("hi")); + +const first = await fetch(server.url + "/"); +check("keeps the connection", first.headers.get("connection"), "keep-alive"); +check("frames the body", first.headers.get("content-length"), "2"); +check("answers", await first.text(), "hi"); + +// The same connection, reused: 50 requests in a row must all be answered. +let ok = 0; +for (let i = 0; i < 50; i++) if ((await fetch(server.url + "/")).status === 200) ok++; +check("serves request after request", ok, 50); + +// A client that asks for the connection to close gets that instead. +const closed = await fetch(server.url + "/", { headers: { connection: "close" } }); +check("honors Connection: close", closed.headers.get("connection"), "close"); + +// stop() has to close live connections too, or the loop never drains. +server.stop(); +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/serve_large_body.mjs b/tests/fixtures/serve_large_body.mjs new file mode 100644 index 0000000..3cb9886 --- /dev/null +++ b/tests/fixtures/serve_large_body.mjs @@ -0,0 +1,28 @@ +// A request body arrives over as many reads as the kernel gives us. Until the +// server accumulated them, anything past one read (about 64KB) reached the +// handler truncated, so a 1MB JSON POST -- an ordinary API request -- failed +// to parse. +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok?"ok ":"FAIL ") + n + " got=" + JSON.stringify(got) + " want=" + JSON.stringify(want)); }; + +const server = Sxn.serve({ port: 0 }, async (req) => { + const body = await req.text(); + return Response.json({ length: body.length, first: body.slice(0, 4), last: body.slice(-4) }); +}); + +for (const size of [1024, 65536, 1024 * 1024, 4 * 1024 * 1024]) { + const body = "ab" + "x".repeat(size - 4) + "yz"; + const res = await fetch(server.url + "/", { method: "POST", headers: { "content-type": "text/plain" }, body }); + const got = await res.json(); + check(`${size} bytes arrive whole`, got.length, body.length); + check(`${size} bytes are intact`, got.first + got.last, "abxxxxyz"); +} + +// A body may contain a NUL byte; its length must not come from strlen. +const withNul = await fetch(server.url + "/", { method: "POST", body: "a\0b" }); +check("a NUL byte does not truncate", (await withNul.json()).length, 3); + +server.stop(); +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/serve_lazy_headers.mjs b/tests/fixtures/serve_lazy_headers.mjs new file mode 100644 index 0000000..5c75360 --- /dev/null +++ b/tests/fixtures/serve_lazy_headers.mjs @@ -0,0 +1,47 @@ +// The headers a served request carries are built on first read. Everything +// about them still has to behave as if they had been built up front. +let bad = 0; +const check = (n, got, want) => { const ok = got === want; if (!ok) bad++; + console.log((ok?"ok ":"FAIL ") + n + " got=" + JSON.stringify(got) + " want=" + JSON.stringify(want)); }; + +const server = Sxn.serve({ port: 0 }, async (req) => { + const url = new URL(req.url); + if (url.pathname === "/read") return Response.json({ + ct: req.headers.get("content-type"), + missing: req.headers.get("x-nope"), + has: req.headers.has("x-marker"), + twice: req.headers.get("x-marker") === req.headers.get("x-marker"), + count: [...req.headers.keys()].length > 0, + }); + if (url.pathname === "/ignore") return new Response("never read them"); + if (url.pathname === "/mutate") { + req.headers.set("x-added", "1"); + return Response.json({ added: req.headers.get("x-added"), marker: req.headers.get("x-marker") }); + } + if (url.pathname === "/clone") { + const copy = req.clone(); + return Response.json({ same: copy.headers.get("x-marker") }); + } + if (url.pathname === "/body") return Response.json({ body: await req.json() }); + return new Response("?", { status: 404 }); +}); + +const call = (path, init) => fetch(server.url + path, init); +const headers = { "x-marker": "here", "content-type": "application/json" }; + +const read = await (await call("/read", { headers })).json(); +check("reads a header", read.ct, "application/json"); +check("a missing header is null", read.missing, null); +check("has() works", read.has, true); +check("reading twice is stable", read.twice, true); +check("iterates", read.count, true); +check("a handler that ignores them still works", await (await call("/ignore", { headers })).text(), "never read them"); +const mutated = await (await call("/mutate", { headers })).json(); +check("can be added to", mutated.added, "1"); +check("and keeps what arrived", mutated.marker, "here"); +check("clone carries them", (await (await call("/clone", { headers })).json()).same, "here"); +check("a body still reads", (await (await call("/body", { method: "POST", headers, body: '{"n":1}' })).json()).body.n, 1); + +server.stop(); +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/tests/fixtures/sxbc_roundtrip.sh b/tests/fixtures/sxbc_roundtrip.sh index 3bb9dfc..9c320b6 100755 --- a/tests/fixtures/sxbc_roundtrip.sh +++ b/tests/fixtures/sxbc_roundtrip.sh @@ -90,6 +90,24 @@ if ! (cd "$DIR/elsewhere" && "$SXN" priv.sxbc >/dev/null 2>&1); then if [ "$code" -gt 1 ]; then bad=1; echo "FAIL moved stripped .sxbc did not run (exit $code)"; fi fi +# ---- a program of more than one file -------------------------------- +# Imports are resolved while a module is compiled, so `sxn compile` needs the +# same module loader running a file does. It had none, and failed on any file +# that imported a sibling -- which is every file in a real program. +cat > "$DIR/lib.sx" <<'LIB' +export const greet = (who: string): string => `hello ${who}`; +LIB +cat > "$DIR/entry.sx" <<'ENTRY' +import { greet } from "./lib.sx"; +console.log(greet("bytecode")); +ENTRY +if ! "$SXN" compile "$DIR/entry.sx" -o "$DIR/entry.sxbc" >/dev/null 2>&1; then + bad=1; echo "FAIL compiling a file with an import failed" +else + out=$("$SXN" "$DIR/entry.sxbc" 2>&1 || true) + [ "$out" = "hello bytecode" ] || { bad=1; echo "FAIL bytecode with an import printed '$out'"; } +fi + # ---- a corrupt/foreign .sxbc is a clean error, not a crash ------------ echo "not bytecode" > "$DIR/bad.sxbc" set +e diff --git a/tests/fixtures/typed_inline.sx b/tests/fixtures/typed_inline.sx new file mode 100644 index 0000000..8a85b16 --- /dev/null +++ b/tests/fixtures/typed_inline.sx @@ -0,0 +1,197 @@ +// A call to a small function with a fully declared scalar signature is +// spliced into its caller (see sx_inline_typed_calls in quickjs.c). These +// checks assert the splice changes nothing observable except the missing +// frame: the same values, the same exceptions, the same behaviour when the +// binding is reassigned, captured, or used as a value. + +let failures = 0; +function check(name: string, actual, expected): void { + if (!Object.is(actual, expected)) { + console.log("FAIL", name, "expected", expected, "got", actual); + failures += 1; + } +} + +// --- the shapes that are meant to inline --------------------------------- +function arith(): void { + function add(a: i32, b: i32): i32 { return a + b; } + function sub(a: f64, b: f64): f64 { return a - b; } + function mul(a: f64, b: f64): f64 { return a * b; } + function less(a: i32, b: i32): bool { return a < b; } + function negate(a: f64): f64 { return -a; } + function scale(a: f64): f64 { return a * 2; } + + check("add", add(2, 3), 5); + check("sub", sub(2.5, 1.25), 1.25); + check("mul", mul(3, 4), 12); + check("less true", less(1, 2), true); + check("less false", less(2, 1), false); + check("negate", negate(5), -5); + check("negate zero keeps sign", negate(0), -0); + check("scale", scale(21), 42); + + // Called in a loop, which is where the frame cost was measured. + let total = 0; + for (let i = 0; i < 5; i++) total = add(total, i); + check("add in a loop", total, 10); +} +arith(); + +// --- values must still flow through unchanged ---------------------------- +function coercion(): void { + function add(a: i32, b: i32): i32 { return a + b; } + // The declared types are not enforced at runtime, so the spliced `add` + // must behave exactly as the call did on values that are not integers. + check("string concat", add("a", "b"), "ab"); + check("undefined is NaN", add(undefined, 1), NaN); + check("object valueOf runs", add({ valueOf() { return 7; } }, 1), 8); + check("overflow promotes", add(2147483647, 1), 2147483648); +} +coercion(); + +// --- exceptions ---------------------------------------------------------- +function throwing(): void { + function add(a: i32, b: i32): i32 { return a + b; } + let threw = ""; + try { add(Symbol("s"), 1); } catch (e) { threw = e.constructor.name; } + check("symbol still throws TypeError", threw, "TypeError"); +} +throwing(); + +// --- a reassigned binding must not be spliced ---------------------------- +function reassigned(): number { + function pick(a: i32, b: i32): i32 { return a + b; } + let r = pick(1, 2); + pick = function (a: i32, b: i32): i32 { return a * b; }; + return r + pick(3, 4); +} +check("reassigned callee", reassigned(), 15); + +// --- a captured binding must not be spliced ------------------------------ +function captured(): number { + function pick(a: i32, b: i32): i32 { return a + b; } + const swap = () => { pick = (a: i32, b: i32): i32 => a * b; }; + const before = pick(2, 3); + swap(); + return before + pick(2, 3); +} +check("captured callee", captured(), 11); + +// --- the function is still an ordinary value ----------------------------- +function stillAValue(): void { + function add(a: i32, b: i32): i32 { return a + b; } + check("direct call", add(1, 1), 2); + check("reduce", [1, 2, 3, 4].reduce(add, 0), 10); + check("apply", add.apply(null, [3, 4]), 7); + check("length", add.length, 2); + check("name", add.name, "add"); +} +stillAValue(); + +// --- shapes that must be left alone -------------------------------------- +function notInlined(): void { + function untyped(a, b) { return a + b; } + function stringy(a: string, b: string): string { return a + b; } + function withLocal(a: i32, b: i32): i32 { let t = a + b; return t; } + function twoUses(a: i32): i32 { return a * a; } + function branching(a: i32, b: i32): i32 { return a > b ? a : b; } + function calling(a: i32): f64 { return Math.abs(a); } + + check("untyped", untyped(1, 2), 3); + check("string", stringy("a", "b"), "ab"); + check("has a local", withLocal(1, 2), 3); + check("uses an arg twice", twoUses(6), 36); + check("branches", branching(3, 9), 9); + check("calls out", calling(-4), 4); + + // Wrong arity on both sides of the declared signature. + function add(a: i32, b: i32): i32 { return a + b; } + check("too few args", add(1), NaN); + check("too many args", add(1, 2, 3), 3); +} +notInlined(); + +// --- argument evaluation order and side effects -------------------------- +function order(): void { + function add(a: i32, b: i32): i32 { return a + b; } + const seen = []; + const t = (n: i32): i32 => { seen.push(n); return n; }; + check("side-effecting args", add(t(1), t(2)), 3); + check("evaluated left to right", seen.join(","), "1,2"); +} +order(); + +// --- recursion still terminates ------------------------------------------ +function recursion(): void { + function fact(n: i32): i32 { return n < 2 ? 1 : n * fact(n - 1); } + check("recursive", fact(5), 120); +} +recursion(); + +// --- differential: inlined vs the identical untyped function ------------- +// Each pair is the same body, once with a declared scalar signature (which +// inlines) and once without (which does not). The calls have to be written +// out rather than driven from a table: reaching a function through `apply`, +// or through an arrow that closes over it, is exactly what stops the splice, +// so a table-driven version would compare two un-inlined functions and prove +// nothing. Over a value table chosen to hit the awkward paths -- the i32 +// boundaries, -0, NaN, the infinities, strings, objects, arrays, null and +// undefined -- the two must agree on every result and on every exception. +function differential(): void { + function t_add(x: f64, y: f64): f64 { return x + y; } + function u_add(x, y) { return x + y; } + function t_div(x: f64, y: f64): f64 { return x / y; } + function u_div(x, y) { return x / y; } + function t_shl(x: f64, y: f64): f64 { return x << y; } + function u_shl(x, y) { return x << y; } + function t_shr(x: f64, y: f64): f64 { return x >>> y; } + function u_shr(x, y) { return x >>> y; } + function t_lt(x: f64, y: f64): f64 { return x < y; } + function u_lt(x, y) { return x < y; } + function t_looseEq(x: f64, y: f64): f64 { return x == y; } + function u_looseEq(x, y) { return x == y; } + + const values = [0, -0, 1, -1, 2147483647, -2147483648, 0.5, 1e21, NaN, + Infinity, -Infinity, "7", "", true, false, null, undefined, + {}, [], [3]]; + let compared = 0; + function compare(name: string, l: string, r: string, a, b): void { + compared += 1; + if (l !== r) { + console.log("FAIL differential", name, String(a), String(b), l, "vs", r); + failures += 1; + } + } + + for (const a of values) { + for (const b of values) { + let l = "", r = ""; + try { l = "v:" + String(t_add(a, b)); } catch (e) { l = "e:" + e.constructor.name; } + try { r = "v:" + String(u_add(a, b)); } catch (e) { r = "e:" + e.constructor.name; } + compare("add", l, r, a, b); + try { l = "v:" + String(t_div(a, b)); } catch (e) { l = "e:" + e.constructor.name; } + try { r = "v:" + String(u_div(a, b)); } catch (e) { r = "e:" + e.constructor.name; } + compare("div", l, r, a, b); + try { l = "v:" + String(t_shl(a, b)); } catch (e) { l = "e:" + e.constructor.name; } + try { r = "v:" + String(u_shl(a, b)); } catch (e) { r = "e:" + e.constructor.name; } + compare("shl", l, r, a, b); + try { l = "v:" + String(t_shr(a, b)); } catch (e) { l = "e:" + e.constructor.name; } + try { r = "v:" + String(u_shr(a, b)); } catch (e) { r = "e:" + e.constructor.name; } + compare("shr", l, r, a, b); + try { l = "v:" + String(t_lt(a, b)); } catch (e) { l = "e:" + e.constructor.name; } + try { r = "v:" + String(u_lt(a, b)); } catch (e) { r = "e:" + e.constructor.name; } + compare("lt", l, r, a, b); + try { l = "v:" + String(t_looseEq(a, b)); } catch (e) { l = "e:" + e.constructor.name; } + try { r = "v:" + String(u_looseEq(a, b)); } catch (e) { r = "e:" + e.constructor.name; } + compare("looseEq", l, r, a, b); + } + } + if (compared !== values.length * values.length * 6) { + console.log("FAIL differential ran", compared, "cases"); + failures += 1; + } +} +differential(); + +if (failures !== 0) throw new Error(failures + " typed-inline checks failed"); +console.log("typed inline: all checks passed"); diff --git a/tests/fixtures/typed_overflow.sx b/tests/fixtures/typed_overflow.sx new file mode 100644 index 0000000..fb57681 --- /dev/null +++ b/tests/fixtures/typed_overflow.sx @@ -0,0 +1,50 @@ +// `safe` now specializes on the type that was declared, not on the `safe` +// keyword alone. i32 wraps at the boundary by definition; every other +// annotation, and an un-annotated `safe`, keeps exact JavaScript arithmetic +// where 2^31-1 + 1 promotes to a double. +// +// Both right-hand-side shapes matter: a constant and a local used to compile +// through different peephole branches and disagreed with each other. + +let failures = 0; +function check(name: string, actual: number, expected: number): void { + if (actual !== expected) { + console.log("FAIL", name, "expected", expected, "got", actual); + failures += 1; + } +} + +function i32ConstRhs(): number { safe let mut n: i32 = 2147483647; n += 1; return n; } +function i32LocalRhs(): number { safe let mut n: i32 = 2147483647; let one = 1; n += one; return n; } +function f64ConstRhs(): number { safe let mut d: f64 = 2147483647; d += 1; return d; } +function f64LocalRhs(): number { safe let mut d: f64 = 2147483647; let one = 1; d += one; return d; } +function bareConstRhs(): number { safe let mut u = 2147483647; u += 1; return u; } +function bareLocalRhs(): number { safe let mut u = 2147483647; let one = 1; u += one; return u; } +function plainLet(): number { let x = 2147483647; let one = 1; x += one; return x; } + +check("i32 const rhs wraps", i32ConstRhs(), -2147483648); +check("i32 local rhs wraps", i32LocalRhs(), -2147483648); +check("f64 const rhs promotes", f64ConstRhs(), 2147483648); +check("f64 local rhs promotes", f64LocalRhs(), 2147483648); +check("un-annotated safe promotes", bareConstRhs(), 2147483648); +check("un-annotated safe promotes", bareLocalRhs(), 2147483648); +check("plain let promotes", plainLet(), 2147483648); + +// The fused counting loop is still reached by a declared i32 accumulator. +function fused(): number { + safe let mut total: i32 = 0; + for (let i = 0; i < 1000; i++) total += i; + return total; +} +check("fused i32 loop", fused(), 499500); + +// A declared f64 accumulator must not reach it, and must stay exact. +function notFused(): number { + safe let mut total: f64 = 0; + for (let i = 0; i < 3; i++) total += 0.5; + return total; +} +check("f64 loop stays exact", notFused(), 1.5); + +if (failures !== 0) throw new Error(failures + " typed-overflow checks failed"); +console.log("typed overflow: all checks passed"); diff --git a/tests/fixtures/wintertc_surface.mjs b/tests/fixtures/wintertc_surface.mjs new file mode 100644 index 0000000..d8274bd --- /dev/null +++ b/tests/fixtures/wintertc_surface.mjs @@ -0,0 +1,87 @@ +// Every name in the Minimum Common API (min-common-api.proposal.wintertc.org) +// that does not live under WebAssembly, checked for presence and, where a +// check is cheap and meaningful, for behaviour. WebAssembly is listed at the +// end as the one part of the surface this runtime does not have. +let bad = 0; +const check = (name, ok, detail) => { + if (!ok) bad++; + console.log((ok ? "ok " : "FAIL ") + name + (detail === undefined ? "" : " " + detail)); +}; + +const interfaces = [ + "AbortController", "AbortSignal", "Event", "EventTarget", "CustomEvent", + "ErrorEvent", "MessageChannel", "MessageEvent", "MessagePort", + "PromiseRejectionEvent", "DOMException", "Headers", "Request", "Response", + "FormData", "Blob", "File", "CompressionStream", "DecompressionStream", + "ByteLengthQueuingStrategy", "CountQueuingStrategy", + "ReadableByteStreamController", "ReadableStream", "ReadableStreamBYOBReader", + "ReadableStreamBYOBRequest", "ReadableStreamDefaultController", + "ReadableStreamDefaultReader", "TransformStream", + "TransformStreamDefaultController", "WritableStream", + "WritableStreamDefaultController", "WritableStreamDefaultWriter", + "TextDecoder", "TextDecoderStream", "TextEncoder", "TextEncoderStream", + "URL", "URLSearchParams", "URLPattern", "Crypto", "CryptoKey", "SubtleCrypto", + "Performance", +]; +const globals = [ + "globalThis", "atob", "btoa", "clearTimeout", "clearInterval", + "queueMicrotask", "reportError", "self", "setTimeout", "setInterval", + "structuredClone", "fetch", "console", "crypto", "performance", "navigator", +]; +for (const name of interfaces) check("interface " + name, typeof globalThis[name] === "function"); +for (const name of globals) check("global " + name, typeof globalThis[name] !== "undefined"); +for (const name of ["onerror", "onunhandledrejection", "onrejectionhandled"]) + check("handler " + name, name in globalThis); +check("navigator.userAgent", typeof navigator.userAgent === "string", navigator.userAgent); +check("total", interfaces.length + globals.length + 3 === 62, String(interfaces.length + globals.length + 3)); + +// Behaviour, not just names. +check("self is the global", self === globalThis); +check("performance is a Performance", performance instanceof Performance); +check("URLPattern segment", new URLPattern({ pathname: "/books/:id" }).exec("https://x.dev/books/7").pathname.groups.id === "7"); +check("URLPattern stops at a slash", new URLPattern({ pathname: "/books/:id" }).test("https://x.dev/books/7/pages") === false); +check("URLPattern wildcard", new URLPattern("https://x.dev/a/*").test("https://x.dev/a/b/c")); +check("URLPattern optional group", new URLPattern({ pathname: "/opt{/:id}?" }).test("https://x.dev/opt")); +check("URLPattern rejects another host", new URLPattern("https://x.dev/a").test("https://y.dev/a") === false); + +const ee = new ErrorEvent("error", { message: "m", filename: "f", lineno: 2 }); +check("ErrorEvent carries its fields", ee.message === "m" && ee.filename === "f" && ee.lineno === 2); +const pre = new PromiseRejectionEvent("unhandledrejection", { reason: "r" }); +check("PromiseRejectionEvent carries its reason", pre.reason === "r"); + +const text = "hello ".repeat(200); +const gz = await new Response(new Blob([text]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(); +check("CompressionStream shrinks", gz.byteLength < text.length, gz.byteLength + " < " + text.length); +const back = await new Response(new Blob([new Uint8Array(gz)]).stream().pipeThrough(new DecompressionStream("gzip"))).text(); +check("DecompressionStream round trip", back === text); +for (const format of ["deflate", "deflate-raw"]) { + const packed = await new Response(new Blob([text]).stream().pipeThrough(new CompressionStream(format))).arrayBuffer(); + const out = await new Response(new Blob([new Uint8Array(packed)]).stream().pipeThrough(new DecompressionStream(format))).text(); + check("round trip " + format, out === text); +} +let threw = false; +try { new CompressionStream("brotli"); } catch { threw = true; } +check("an unknown format throws", threw); + +const bytes = new ReadableStream({ type: "bytes", start(c) { c.enqueue(new Uint8Array([1, 2, 3, 4, 5])); c.close(); } }); +const reader = bytes.getReader({ mode: "byob" }); +const first = await reader.read(new Uint8Array(2)); +const second = await reader.read(new Uint8Array(8)); +check("byob fills the view given", String(Array.from(first.value)) === "1,2"); +check("byob keeps the rest", String(Array.from(second.value)) === "3,4,5"); +check("byob ends", (await reader.read(new Uint8Array(4))).done === true); + +const tsc = []; +await new ReadableStream({ start(c) { c.enqueue("a"); c.close(); } }) + .pipeThrough(new TransformStream({ + transform(chunk, controller) { + check("transform controller has a class", controller instanceof TransformStreamDefaultController); + controller.enqueue(chunk.toUpperCase()); + }, + })) + .pipeTo(new WritableStream({ write(c) { tsc.push(c); } })); +check("transform ran", tsc.join("") === "A"); + +check("WebAssembly is the gap", typeof globalThis.WebAssembly === "undefined"); +console.log(bad === 0 ? "ALL PASS" : "FAILURES: " + bad); +process.exit(bad === 0 ? 0 : 1); diff --git a/third_party/quickjs/builtin-array-fromasync.h b/third_party/quickjs/builtin-array-fromasync.h index a0a5a97..123e869 100644 --- a/third_party/quickjs/builtin-array-fromasync.h +++ b/third_party/quickjs/builtin-array-fromasync.h @@ -5,7 +5,7 @@ const uint32_t qjsc_builtin_array_fromasync_size = 857; const uint8_t qjsc_builtin_array_fromasync[857] = { - 0x1b, 0x9c, 0x60, 0xc8, 0x7e, 0x0e, 0x01, 0x28, + 0x1b, 0xd8, 0x4d, 0x82, 0x8c, 0x0e, 0x01, 0x28, 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0xb7, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x49, 0x74, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x01, 0x2a, 0x4f, 0x62, @@ -25,40 +25,40 @@ const uint8_t qjsc_builtin_array_fromasync[857] = { 0x08, 0x69, 0x74, 0x65, 0x72, 0x01, 0x1c, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x01, 0x08, 0x63, - 0x61, 0x6c, 0x6c, 0x0c, 0x00, 0x02, 0x00, 0xb0, + 0x61, 0x6c, 0x6c, 0x0c, 0x00, 0x02, 0x00, 0xb8, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, - 0x04, 0x01, 0xb2, 0x01, 0x00, 0x00, 0x00, 0x0c, + 0x04, 0x01, 0xba, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x43, 0x02, 0x01, 0x00, 0x05, 0x00, 0x05, 0x01, - 0x05, 0x00, 0x01, 0x03, 0x05, 0xc6, 0x02, 0x00, - 0x01, 0x80, 0x03, 0xbc, 0x03, 0x00, 0x01, 0x80, - 0x00, 0xfa, 0x03, 0x00, 0x01, 0x80, 0x01, 0xfc, - 0x03, 0x00, 0x01, 0x80, 0x04, 0xfe, 0x03, 0x00, - 0x01, 0x80, 0x02, 0x0c, 0x60, 0x02, 0x01, 0x90, + 0x05, 0x00, 0x01, 0x03, 0x05, 0xce, 0x02, 0x00, + 0x01, 0x80, 0x03, 0xc4, 0x03, 0x00, 0x01, 0x80, + 0x00, 0x82, 0x04, 0x00, 0x01, 0x80, 0x01, 0x84, + 0x04, 0x00, 0x01, 0x80, 0x04, 0x86, 0x04, 0x00, + 0x01, 0x80, 0x02, 0x0c, 0x60, 0x02, 0x01, 0x98, 0x02, 0x03, 0x0e, 0x01, 0x06, 0x00, 0x05, 0x00, - 0xf4, 0x03, 0x11, 0x80, 0x04, 0x00, 0x01, 0x00, - 0x82, 0x04, 0x00, 0x01, 0x00, 0x84, 0x04, 0x00, - 0x01, 0x00, 0x80, 0x04, 0x01, 0xff, 0xff, 0xff, - 0xff, 0x0f, 0x40, 0x82, 0x04, 0x01, 0x01, 0x40, - 0x84, 0x04, 0x01, 0x02, 0x40, 0x86, 0x04, 0x02, - 0x00, 0x40, 0x88, 0x04, 0x02, 0x04, 0x40, 0x8a, - 0x04, 0x02, 0x05, 0x40, 0x8c, 0x04, 0x02, 0x06, - 0x40, 0x8e, 0x04, 0x02, 0x07, 0x40, 0x6e, 0x06, - 0x08, 0x40, 0x90, 0x01, 0x07, 0x09, 0x40, 0x90, - 0x04, 0x0a, 0x08, 0x50, 0x90, 0x01, 0x0d, 0x0b, - 0x40, 0xea, 0x01, 0x0d, 0x0c, 0x40, 0x10, 0x00, - 0x01, 0x00, 0xbc, 0x03, 0x01, 0x01, 0xfa, 0x03, - 0x02, 0x01, 0xfe, 0x03, 0x04, 0x01, 0xc6, 0x02, - 0x00, 0x01, 0xfc, 0x03, 0x03, 0x01, 0x08, 0xcd, + 0xf4, 0x03, 0x11, 0x88, 0x04, 0x00, 0x01, 0x00, + 0x8a, 0x04, 0x00, 0x01, 0x00, 0x8c, 0x04, 0x00, + 0x01, 0x00, 0x88, 0x04, 0x01, 0xff, 0xff, 0xff, + 0xff, 0x0f, 0x40, 0x8a, 0x04, 0x01, 0x01, 0x40, + 0x8c, 0x04, 0x01, 0x02, 0x40, 0x8e, 0x04, 0x02, + 0x00, 0x40, 0x90, 0x04, 0x02, 0x04, 0x40, 0x92, + 0x04, 0x02, 0x05, 0x40, 0x94, 0x04, 0x02, 0x06, + 0x40, 0x96, 0x04, 0x02, 0x07, 0x40, 0x76, 0x06, + 0x08, 0x40, 0x98, 0x01, 0x07, 0x09, 0x40, 0x98, + 0x04, 0x0a, 0x08, 0x50, 0x98, 0x01, 0x0d, 0x0b, + 0x40, 0xf2, 0x01, 0x0d, 0x0c, 0x40, 0x10, 0x00, + 0x01, 0x00, 0xc4, 0x03, 0x01, 0x01, 0x82, 0x04, + 0x02, 0x01, 0x86, 0x04, 0x04, 0x01, 0xce, 0x02, + 0x00, 0x01, 0x84, 0x04, 0x03, 0x01, 0x08, 0xcd, 0x0d, 0x60, 0x02, 0x00, 0x60, 0x01, 0x00, 0x60, 0x00, 0x00, 0xdb, 0xd3, 0xdc, 0x11, 0xfc, 0xf4, - 0x08, 0x0e, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xe4, + 0x08, 0x0e, 0x38, 0x51, 0x00, 0x00, 0x00, 0xe4, 0xd4, 0xdd, 0x11, 0xfc, 0xf4, 0x08, 0x0e, 0x38, - 0x4d, 0x00, 0x00, 0x00, 0xe5, 0xd5, 0x60, 0x07, + 0x51, 0x00, 0x00, 0x00, 0xe5, 0xd5, 0x60, 0x07, 0x00, 0x60, 0x06, 0x00, 0x60, 0x05, 0x00, 0x60, - 0x04, 0x00, 0x60, 0x03, 0x00, 0xdc, 0x38, 0x4d, + 0x04, 0x00, 0x60, 0x03, 0x00, 0xdc, 0x38, 0x51, 0x00, 0x00, 0x00, 0xb1, 0xf4, 0x16, 0xdc, 0x99, 0x04, 0x1b, 0x00, 0x00, 0x00, 0xb1, 0xf4, 0x0c, - 0xe7, 0x11, 0x04, 0x09, 0x01, 0x00, 0x00, 0x21, + 0xe7, 0x11, 0x04, 0x0d, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x06, 0xd6, 0xbf, 0xcd, 0x04, 0xcc, 0x0d, 0xff, 0xcd, 0x05, 0x09, 0xcd, 0x06, 0xdb, 0xe8, 0x46, 0xcd, 0x07, 0x61, 0x07, 0x00, @@ -66,7 +66,7 @@ const uint8_t qjsc_builtin_array_fromasync[857] = { 0xdb, 0xe9, 0x46, 0x62, 0x07, 0x00, 0x61, 0x07, 0x00, 0x07, 0xae, 0x68, 0x9e, 0x00, 0x00, 0x00, 0x60, 0x08, 0x00, 0x06, 0x11, 0xfc, 0xf5, 0x0c, - 0x70, 0x41, 0x37, 0x00, 0x00, 0x00, 0xcd, 0x08, + 0x70, 0x41, 0x3b, 0x00, 0x00, 0x00, 0xcd, 0x08, 0x0e, 0xf6, 0x05, 0x0e, 0xdb, 0xf6, 0xf2, 0x61, 0x08, 0x00, 0x8d, 0x11, 0xf5, 0x03, 0x0e, 0xbf, 0x62, 0x08, 0x00, 0x61, 0x05, 0x00, 0xf4, 0x0c, @@ -77,40 +77,40 @@ const uint8_t qjsc_builtin_array_fromasync[857] = { 0x09, 0x00, 0xdb, 0x61, 0x04, 0x00, 0x46, 0xcd, 0x09, 0x61, 0x06, 0x00, 0xf4, 0x08, 0x61, 0x09, 0x00, 0x8b, 0x62, 0x09, 0x00, 0xdc, 0xf4, 0x15, - 0xdc, 0x41, 0x0a, 0x01, 0x00, 0x00, 0xdd, 0x61, + 0xdc, 0x41, 0x0e, 0x01, 0x00, 0x00, 0xdd, 0x61, 0x09, 0x00, 0x61, 0x04, 0x00, 0x24, 0x03, 0x00, 0x8b, 0x62, 0x09, 0x00, 0x5d, 0x04, 0x00, 0x61, 0x03, 0x00, 0x61, 0x04, 0x00, 0x91, 0x62, 0x04, - 0x00, 0x0b, 0x61, 0x09, 0x00, 0x4b, 0x48, 0x00, - 0x00, 0x00, 0x0a, 0x4b, 0x45, 0x00, 0x00, 0x00, - 0x0a, 0x4b, 0x46, 0x00, 0x00, 0x00, 0xfb, 0x0e, + 0x00, 0x0b, 0x61, 0x09, 0x00, 0x4b, 0x4c, 0x00, + 0x00, 0x00, 0x0a, 0x4b, 0x49, 0x00, 0x00, 0x00, + 0x0a, 0x4b, 0x4a, 0x00, 0x00, 0x00, 0xfb, 0x0e, 0xf6, 0xa2, 0x60, 0x0a, 0x00, 0x61, 0x07, 0x00, - 0x41, 0x0a, 0x01, 0x00, 0x00, 0xdb, 0x24, 0x01, + 0x41, 0x0e, 0x01, 0x00, 0x00, 0xdb, 0x24, 0x01, 0x00, 0xcd, 0x0a, 0x61, 0x05, 0x00, 0xf4, 0x09, 0xcc, 0x0d, 0x11, 0x21, 0x00, 0x00, 0xf6, 0x03, 0xea, 0xf8, 0x62, 0x03, 0x00, 0x6b, 0x88, 0x00, 0x00, 0x00, 0x60, 0x0c, 0x00, 0x60, 0x0b, 0x00, - 0x06, 0x11, 0xfc, 0xf5, 0x13, 0x70, 0x41, 0x48, - 0x00, 0x00, 0x00, 0xcd, 0x0b, 0x41, 0x75, 0x00, + 0x06, 0x11, 0xfc, 0xf5, 0x13, 0x70, 0x41, 0x4c, + 0x00, 0x00, 0x00, 0xcd, 0x0b, 0x41, 0x79, 0x00, 0x00, 0x00, 0xcd, 0x0c, 0x0e, 0xf6, 0x10, 0x0e, - 0x61, 0x0a, 0x00, 0x41, 0x76, 0x00, 0x00, 0x00, + 0x61, 0x0a, 0x00, 0x41, 0x7a, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x8b, 0xf6, 0xe0, 0x61, 0x0c, 0x00, 0xf5, 0x4a, 0x61, 0x06, 0x00, 0xf4, 0x08, 0x61, 0x0b, 0x00, 0x8b, 0x62, 0x0b, 0x00, 0xdc, - 0xf4, 0x15, 0xdc, 0x41, 0x0a, 0x01, 0x00, 0x00, + 0xf4, 0x15, 0xdc, 0x41, 0x0e, 0x01, 0x00, 0x00, 0xdd, 0x61, 0x0b, 0x00, 0x61, 0x04, 0x00, 0x24, 0x03, 0x00, 0x8b, 0x62, 0x0b, 0x00, 0x5d, 0x04, 0x00, 0x61, 0x03, 0x00, 0x61, 0x04, 0x00, 0x91, 0x62, 0x04, 0x00, 0x0b, 0x61, 0x0b, 0x00, 0x4b, - 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x45, 0x00, - 0x00, 0x00, 0x0a, 0x4b, 0x46, 0x00, 0x00, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x49, 0x00, + 0x00, 0x00, 0x0a, 0x4b, 0x4a, 0x00, 0x00, 0x00, 0xfb, 0x0e, 0xf6, 0x87, 0x0e, 0x06, 0x6c, 0x0d, 0x00, 0x00, 0x00, 0x0e, 0xf6, 0x1e, 0x6c, 0x05, 0x00, 0x00, 0x00, 0x30, 0x61, 0x0a, 0x00, 0x40, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x0d, 0x61, 0x0a, 0x00, 0x41, 0x06, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x0e, 0x6d, 0x61, 0x03, 0x00, 0x61, 0x04, - 0x00, 0x42, 0x37, 0x00, 0x00, 0x00, 0x61, 0x03, + 0x00, 0x42, 0x3b, 0x00, 0x00, 0x00, 0x61, 0x03, 0x00, 0x2f, 0xca, 0x00, 0x28, 0xca, 0x00, 0xd7, 0x28, }; diff --git a/third_party/quickjs/builtin-iterator-zip-keyed.h b/third_party/quickjs/builtin-iterator-zip-keyed.h index dc9f56a..e982402 100644 --- a/third_party/quickjs/builtin-iterator-zip-keyed.h +++ b/third_party/quickjs/builtin-iterator-zip-keyed.h @@ -5,7 +5,7 @@ const uint32_t qjsc_builtin_iterator_zip_keyed_size = 2490; const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { - 0x1b, 0x50, 0x00, 0x79, 0x12, 0x2b, 0x01, 0x1c, + 0x1b, 0xf5, 0x97, 0xc2, 0xb5, 0x2b, 0x01, 0x1c, 0x49, 0x74, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x48, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x01, 0x08, 0x63, 0x61, 0x6c, 0x6c, 0x01, 0x24, 0x68, 0x61, @@ -55,95 +55,95 @@ const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x01, 0x18, 0x62, 0x61, 0x64, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x0c, 0x00, 0x02, - 0x00, 0xb0, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x04, 0x01, 0xb2, 0x01, 0x00, 0x00, + 0x00, 0xb8, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x04, 0x01, 0xba, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x43, 0x02, 0x00, 0x00, 0x07, 0x03, - 0x07, 0x01, 0x0a, 0x00, 0x04, 0x0c, 0x0a, 0xfa, - 0x03, 0x00, 0x01, 0x80, 0x09, 0xc0, 0x03, 0x00, - 0x01, 0x80, 0x03, 0xbc, 0x03, 0x00, 0x01, 0x80, - 0x00, 0xfc, 0x03, 0x00, 0x01, 0x80, 0x01, 0xfe, - 0x03, 0x00, 0x01, 0x80, 0x07, 0x80, 0x04, 0x00, - 0x01, 0x80, 0x06, 0x82, 0x04, 0x00, 0x01, 0x80, - 0x08, 0x84, 0x04, 0x00, 0x00, 0x80, 0x05, 0x86, - 0x04, 0x00, 0x01, 0x80, 0x02, 0x88, 0x04, 0x00, - 0x02, 0x80, 0x04, 0x0c, 0x43, 0x02, 0x00, 0x84, + 0x07, 0x01, 0x0a, 0x00, 0x04, 0x0c, 0x0a, 0x82, + 0x04, 0x00, 0x01, 0x80, 0x09, 0xc8, 0x03, 0x00, + 0x01, 0x80, 0x03, 0xc4, 0x03, 0x00, 0x01, 0x80, + 0x00, 0x84, 0x04, 0x00, 0x01, 0x80, 0x01, 0x86, + 0x04, 0x00, 0x01, 0x80, 0x07, 0x88, 0x04, 0x00, + 0x01, 0x80, 0x06, 0x8a, 0x04, 0x00, 0x01, 0x80, + 0x08, 0x8c, 0x04, 0x00, 0x00, 0x80, 0x05, 0x8e, + 0x04, 0x00, 0x01, 0x80, 0x02, 0x90, 0x04, 0x00, + 0x02, 0x80, 0x04, 0x0c, 0x43, 0x02, 0x00, 0x8c, 0x04, 0x02, 0x00, 0x02, 0x03, 0x00, 0x01, 0x00, - 0x17, 0x02, 0x8a, 0x04, 0x00, 0x01, 0x00, 0x8c, - 0x04, 0x00, 0x01, 0x00, 0xbc, 0x03, 0x02, 0x01, - 0xdb, 0x99, 0x04, 0x51, 0x00, 0x00, 0x00, 0xb0, + 0x17, 0x02, 0x92, 0x04, 0x00, 0x01, 0x00, 0x94, + 0x04, 0x00, 0x01, 0x00, 0xc4, 0x03, 0x02, 0x01, + 0xdb, 0x99, 0x04, 0x55, 0x00, 0x00, 0x00, 0xb0, 0xf4, 0x07, 0xdb, 0x07, 0xb1, 0xf4, 0x02, 0x29, 0xe7, 0x11, 0xdc, 0x21, 0x01, 0x00, 0x30, 0x0c, - 0x43, 0x02, 0x00, 0x86, 0x04, 0x01, 0x02, 0x01, - 0x04, 0x00, 0x01, 0x00, 0x2e, 0x03, 0x8e, 0x04, - 0x00, 0x01, 0x00, 0x90, 0x04, 0x02, 0x00, 0x40, - 0x92, 0x04, 0x05, 0x00, 0x03, 0xfc, 0x03, 0x03, + 0x43, 0x02, 0x00, 0x8e, 0x04, 0x01, 0x02, 0x01, + 0x04, 0x00, 0x01, 0x00, 0x2e, 0x03, 0x96, 0x04, + 0x00, 0x01, 0x00, 0x98, 0x04, 0x02, 0x00, 0x40, + 0x9a, 0x04, 0x05, 0x00, 0x03, 0x84, 0x04, 0x03, 0x01, 0x6b, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xdb, 0x98, 0xf4, 0x04, 0x06, 0x6e, 0x28, 0xdb, 0x40, 0x06, 0x00, 0x00, 0x00, 0xd3, 0x61, 0x00, 0x00, 0xf4, 0x08, 0xe7, 0xdb, 0x61, 0x00, 0x00, 0xfa, 0x0e, 0x0e, 0x29, 0xd4, 0x6b, 0x07, 0x00, 0x00, 0x00, 0xd0, 0x6e, 0x28, 0x30, 0x0c, - 0x43, 0x02, 0x00, 0x88, 0x04, 0x02, 0x04, 0x02, - 0x03, 0x00, 0x01, 0x00, 0x53, 0x06, 0x94, 0x04, - 0x00, 0x01, 0x00, 0x96, 0x04, 0x00, 0x01, 0x00, - 0x98, 0x04, 0x01, 0x00, 0x40, 0x9a, 0x04, 0x02, - 0x01, 0x40, 0x8e, 0x04, 0x03, 0x02, 0x40, 0x92, - 0x04, 0x03, 0x03, 0x40, 0x86, 0x04, 0x01, 0x00, - 0x60, 0x00, 0x00, 0x38, 0x4d, 0x00, 0x00, 0x00, + 0x43, 0x02, 0x00, 0x90, 0x04, 0x02, 0x04, 0x02, + 0x03, 0x00, 0x01, 0x00, 0x53, 0x06, 0x9c, 0x04, + 0x00, 0x01, 0x00, 0x9e, 0x04, 0x00, 0x01, 0x00, + 0xa0, 0x04, 0x01, 0x00, 0x40, 0xa2, 0x04, 0x02, + 0x01, 0x40, 0x96, 0x04, 0x03, 0x02, 0x40, 0x9a, + 0x04, 0x03, 0x03, 0x40, 0x8e, 0x04, 0x01, 0x00, + 0x60, 0x00, 0x00, 0x38, 0x51, 0x00, 0x00, 0x00, 0xd3, 0x60, 0x01, 0x00, 0xdc, 0xd4, 0x61, 0x01, 0x00, 0x90, 0x62, 0x01, 0x00, 0xbf, 0xaa, 0xf4, 0x37, 0x60, 0x03, 0x00, 0x60, 0x02, 0x00, 0xdb, 0x61, 0x01, 0x00, 0x46, 0xd5, 0xdb, 0x61, 0x01, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, - 0x1b, 0x1b, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x1b, + 0x1b, 0x1b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0xe7, 0x61, 0x02, 0x00, 0xf9, 0xd6, 0x61, 0x00, 0x00, 0x98, 0xf4, 0xc8, 0x61, 0x03, 0x00, 0x62, 0x00, 0x00, 0xf6, 0xc0, 0x61, - 0x00, 0x00, 0x28, 0x0c, 0x41, 0x02, 0x00, 0xc2, + 0x00, 0x00, 0x28, 0x0c, 0x41, 0x02, 0x00, 0xca, 0x02, 0x02, 0x15, 0x01, 0x06, 0x08, 0x09, 0x02, - 0x9d, 0x05, 0x17, 0x9c, 0x04, 0x00, 0x01, 0x00, - 0x9e, 0x04, 0x00, 0x01, 0x00, 0x9c, 0x04, 0x01, - 0xff, 0xff, 0xff, 0xff, 0x0f, 0x40, 0x9e, 0x04, - 0x01, 0x01, 0x40, 0xa0, 0x04, 0x02, 0x00, 0xc0, - 0x04, 0xa2, 0x04, 0x02, 0x03, 0x40, 0x6a, 0x02, - 0x04, 0xc0, 0x02, 0x96, 0x04, 0x02, 0x05, 0xc0, - 0x01, 0x94, 0x04, 0x02, 0x06, 0xc0, 0x03, 0xa4, - 0x04, 0x02, 0x07, 0xc0, 0x06, 0xa6, 0x04, 0x02, - 0x08, 0xc0, 0x05, 0xa8, 0x04, 0x09, 0x15, 0x40, - 0x9a, 0x04, 0x0b, 0x15, 0x40, 0xaa, 0x04, 0x0c, - 0x0b, 0x40, 0xa8, 0x04, 0x0c, 0x0c, 0x40, 0x8e, - 0x04, 0x0e, 0x0d, 0x40, 0x90, 0x04, 0x10, 0x0e, - 0x40, 0xac, 0x04, 0x14, 0x0d, 0x40, 0x9a, 0x04, - 0x19, 0x15, 0x40, 0x9a, 0x04, 0x1b, 0x15, 0x40, - 0x92, 0x04, 0x1c, 0x15, 0x03, 0xae, 0x04, 0x02, - 0x09, 0xc0, 0x00, 0xb0, 0x04, 0x02, 0x14, 0xc0, - 0x07, 0xbc, 0x03, 0x02, 0x01, 0xc0, 0x03, 0x01, - 0x01, 0xfc, 0x03, 0x03, 0x01, 0x88, 0x04, 0x02, - 0x00, 0x84, 0x04, 0x00, 0x00, 0x80, 0x04, 0x05, - 0x01, 0xfe, 0x03, 0x04, 0x01, 0x82, 0x04, 0x06, - 0x01, 0xfa, 0x03, 0x00, 0x01, 0x0c, 0x42, 0x03, + 0x9d, 0x05, 0x17, 0xa4, 0x04, 0x00, 0x01, 0x00, + 0xa6, 0x04, 0x00, 0x01, 0x00, 0xa4, 0x04, 0x01, + 0xff, 0xff, 0xff, 0xff, 0x0f, 0x40, 0xa6, 0x04, + 0x01, 0x01, 0x40, 0xa8, 0x04, 0x02, 0x00, 0xc0, + 0x04, 0xaa, 0x04, 0x02, 0x03, 0x40, 0x72, 0x02, + 0x04, 0xc0, 0x02, 0x9e, 0x04, 0x02, 0x05, 0xc0, + 0x01, 0x9c, 0x04, 0x02, 0x06, 0xc0, 0x03, 0xac, + 0x04, 0x02, 0x07, 0xc0, 0x06, 0xae, 0x04, 0x02, + 0x08, 0xc0, 0x05, 0xb0, 0x04, 0x09, 0x15, 0x40, + 0xa2, 0x04, 0x0b, 0x15, 0x40, 0xb2, 0x04, 0x0c, + 0x0b, 0x40, 0xb0, 0x04, 0x0c, 0x0c, 0x40, 0x96, + 0x04, 0x0e, 0x0d, 0x40, 0x98, 0x04, 0x10, 0x0e, + 0x40, 0xb4, 0x04, 0x14, 0x0d, 0x40, 0xa2, 0x04, + 0x19, 0x15, 0x40, 0xa2, 0x04, 0x1b, 0x15, 0x40, + 0x9a, 0x04, 0x1c, 0x15, 0x03, 0xb6, 0x04, 0x02, + 0x09, 0xc0, 0x00, 0xb8, 0x04, 0x02, 0x14, 0xc0, + 0x07, 0xc4, 0x03, 0x02, 0x01, 0xc8, 0x03, 0x01, + 0x01, 0x84, 0x04, 0x03, 0x01, 0x90, 0x04, 0x02, + 0x00, 0x8c, 0x04, 0x00, 0x00, 0x88, 0x04, 0x05, + 0x01, 0x86, 0x04, 0x04, 0x01, 0x8a, 0x04, 0x06, + 0x01, 0x82, 0x04, 0x00, 0x01, 0x0c, 0x42, 0x03, 0x00, 0x00, 0x00, 0x09, 0x00, 0x05, 0x00, 0x0c, - 0x00, 0xd5, 0x04, 0x09, 0xb2, 0x04, 0x01, 0x00, - 0x40, 0xee, 0x01, 0x01, 0x01, 0x40, 0xb4, 0x04, - 0x01, 0x02, 0x40, 0x9a, 0x04, 0x03, 0x03, 0x40, - 0xa8, 0x04, 0x04, 0x04, 0x40, 0x8e, 0x04, 0x04, - 0x05, 0x40, 0xb6, 0x04, 0x04, 0x06, 0x40, 0x92, - 0x04, 0x09, 0x07, 0x03, 0x98, 0x04, 0x10, 0x07, - 0x40, 0xae, 0x04, 0x13, 0x10, 0xbc, 0x03, 0x00, - 0x02, 0xc0, 0x03, 0x01, 0x02, 0x96, 0x04, 0x05, - 0x10, 0x6a, 0x04, 0x10, 0x94, 0x04, 0x06, 0x10, - 0xa0, 0x04, 0x02, 0x10, 0xa6, 0x04, 0x08, 0x10, - 0xfc, 0x03, 0x02, 0x02, 0xa4, 0x04, 0x07, 0x10, - 0xb0, 0x04, 0x14, 0x10, 0x88, 0x04, 0x03, 0x02, + 0x00, 0xd5, 0x04, 0x09, 0xba, 0x04, 0x01, 0x00, + 0x40, 0xf6, 0x01, 0x01, 0x01, 0x40, 0xbc, 0x04, + 0x01, 0x02, 0x40, 0xa2, 0x04, 0x03, 0x03, 0x40, + 0xb0, 0x04, 0x04, 0x04, 0x40, 0x96, 0x04, 0x04, + 0x05, 0x40, 0xbe, 0x04, 0x04, 0x06, 0x40, 0x9a, + 0x04, 0x09, 0x07, 0x03, 0xa0, 0x04, 0x10, 0x07, + 0x40, 0xb6, 0x04, 0x13, 0x10, 0xc4, 0x03, 0x00, + 0x02, 0xc8, 0x03, 0x01, 0x02, 0x9e, 0x04, 0x05, + 0x10, 0x72, 0x04, 0x10, 0x9c, 0x04, 0x06, 0x10, + 0xa8, 0x04, 0x02, 0x10, 0xae, 0x04, 0x08, 0x10, + 0x84, 0x04, 0x02, 0x02, 0xac, 0x04, 0x07, 0x10, + 0xb8, 0x04, 0x14, 0x10, 0x90, 0x04, 0x03, 0x02, 0x60, 0x02, 0x00, 0x60, 0x01, 0x00, 0x60, 0x00, 0x00, 0x64, 0x00, 0x00, 0x11, 0xbf, 0xb0, 0xf5, 0x06, 0x11, 0xc0, 0xb0, 0xf4, 0x07, 0xc1, 0x65, 0x00, 0x00, 0xf6, 0x33, 0x11, 0xc1, 0xb0, 0xf4, - 0x0c, 0xe8, 0x11, 0x04, 0x1c, 0x01, 0x00, 0x00, + 0x0c, 0xe8, 0x11, 0x04, 0x20, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x11, 0xc2, 0xb0, 0xf4, - 0x13, 0x0b, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x4b, - 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x75, 0x00, - 0x00, 0x00, 0x28, 0xe9, 0x11, 0x04, 0x1d, 0x01, + 0x13, 0x0b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x4b, + 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x79, 0x00, + 0x00, 0x00, 0x28, 0xe9, 0x11, 0x04, 0x21, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x0e, 0xbf, 0xd3, 0xbf, 0xd4, 0x0c, 0x07, 0xd5, 0x60, 0x03, 0x00, 0xbf, 0xd6, 0x61, 0x03, 0x00, 0x64, 0x03, @@ -152,8 +152,8 @@ const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { 0x64, 0x04, 0x00, 0x61, 0x03, 0x00, 0x46, 0xcd, 0x04, 0x64, 0x05, 0x00, 0x61, 0x03, 0x00, 0x46, 0xcd, 0x05, 0x61, 0x05, 0x00, 0x98, 0xf4, 0x34, - 0x64, 0x06, 0x00, 0x04, 0x1e, 0x01, 0x00, 0x00, - 0xb1, 0xf4, 0x0c, 0xe9, 0x11, 0x04, 0x1d, 0x01, + 0x64, 0x06, 0x00, 0x04, 0x22, 0x01, 0x00, 0x00, + 0xb1, 0xf4, 0x0c, 0xe9, 0x11, 0x04, 0x21, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x61, 0x02, 0x00, 0x61, 0x04, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x64, 0x07, 0x00, @@ -165,98 +165,98 @@ const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { 0x07, 0x6b, 0x2e, 0x00, 0x00, 0x00, 0xbf, 0x65, 0x0a, 0x00, 0x64, 0x05, 0x00, 0x61, 0x03, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, - 0x1b, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x1b, 0x72, + 0x1b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x5d, 0x0b, 0x00, 0x64, 0x05, 0x00, 0x64, 0x03, 0x00, 0xfa, 0x0e, 0xcc, 0x07, 0x30, - 0x30, 0x61, 0x06, 0x00, 0x40, 0x75, 0x00, 0x00, + 0x30, 0x61, 0x06, 0x00, 0x40, 0x79, 0x00, 0x00, 0x00, 0x98, 0xf4, 0x48, 0x64, 0x06, 0x00, 0x04, - 0x1f, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x1d, 0xbf, + 0x23, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x1d, 0xbf, 0xb2, 0x00, 0x02, 0xf4, 0x17, 0x5d, 0x0b, 0x00, 0x64, 0x05, 0x00, 0x64, 0x03, 0x00, 0xfa, 0x0e, - 0xe8, 0x11, 0x04, 0x20, 0x01, 0x00, 0x00, 0x21, + 0xe8, 0x11, 0x04, 0x24, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x61, 0x02, 0x00, 0x61, 0x04, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, - 0x1b, 0x1b, 0x61, 0x06, 0x00, 0x40, 0x48, 0x00, + 0x1b, 0x1b, 0x61, 0x06, 0x00, 0x40, 0x4c, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x93, 0x01, 0xf7, 0xc6, 0x00, 0x64, 0x0a, 0x00, 0x90, 0x65, 0x0a, 0x00, 0x0e, 0x93, 0x00, 0x64, 0x05, 0x00, 0x61, 0x03, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, - 0x1b, 0x72, 0x1b, 0x1b, 0x38, 0x4d, 0x00, 0x00, + 0x1b, 0x72, 0x1b, 0x1b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x64, 0x06, 0x00, - 0x60, 0x08, 0x00, 0x11, 0x04, 0x21, 0x01, 0x00, + 0x60, 0x08, 0x00, 0x11, 0x04, 0x25, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x2c, 0x5d, 0x0b, 0x00, 0x64, 0x05, 0x00, 0x64, 0x03, 0x00, 0xfa, 0xcd, 0x08, 0x61, 0x08, 0x00, 0xf4, 0x05, 0x61, 0x08, 0x00, - 0x30, 0xc2, 0x65, 0x00, 0x00, 0x0b, 0x38, 0x4d, - 0x00, 0x00, 0x00, 0x4b, 0x48, 0x00, 0x00, 0x00, - 0x0a, 0x4b, 0x75, 0x00, 0x00, 0x00, 0x28, 0x11, - 0x04, 0x1e, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x3a, + 0x30, 0xc2, 0x65, 0x00, 0x00, 0x0b, 0x38, 0x51, + 0x00, 0x00, 0x00, 0x4b, 0x4c, 0x00, 0x00, 0x00, + 0x0a, 0x4b, 0x79, 0x00, 0x00, 0x00, 0x28, 0x11, + 0x04, 0x22, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x3a, 0x64, 0x0a, 0x00, 0xc0, 0xa8, 0xf4, 0x17, 0xc2, - 0x65, 0x00, 0x00, 0x0b, 0x38, 0x4d, 0x00, 0x00, - 0x00, 0x4b, 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, - 0x75, 0x00, 0x00, 0x00, 0x28, 0x61, 0x02, 0x00, + 0x65, 0x00, 0x00, 0x0b, 0x38, 0x51, 0x00, 0x00, + 0x00, 0x4b, 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, + 0x79, 0x00, 0x00, 0x00, 0x28, 0x61, 0x02, 0x00, 0x61, 0x04, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x64, 0x07, 0x00, 0x61, 0x03, 0x00, 0x46, 0x1b, 0x72, 0x1b, 0x48, 0xf6, - 0x26, 0x11, 0x04, 0x1f, 0x01, 0x00, 0x00, 0xb0, + 0x26, 0x11, 0x04, 0x23, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x1d, 0xbf, 0xb2, 0x01, 0x02, 0xf4, 0x17, 0x5d, 0x0b, 0x00, 0x64, 0x05, 0x00, 0x64, 0x03, - 0x00, 0xfa, 0x0e, 0xe8, 0x11, 0x04, 0x20, 0x01, + 0x00, 0xfa, 0x0e, 0xe8, 0x11, 0x04, 0x24, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x0e, 0x93, 0x03, 0xf7, 0x39, 0xfe, 0x61, 0x01, 0x00, 0xbf, 0xb0, 0xf4, 0x17, 0xc2, 0x65, 0x00, 0x00, 0x0b, - 0x38, 0x4d, 0x00, 0x00, 0x00, 0x4b, 0x48, 0x00, - 0x00, 0x00, 0x0a, 0x4b, 0x75, 0x00, 0x00, 0x00, + 0x38, 0x51, 0x00, 0x00, 0x00, 0x4b, 0x4c, 0x00, + 0x00, 0x00, 0x0a, 0x4b, 0x79, 0x00, 0x00, 0x00, 0x28, 0xc0, 0x65, 0x00, 0x00, 0x0b, 0x61, 0x02, - 0x00, 0x4b, 0x48, 0x00, 0x00, 0x00, 0x09, 0x4b, - 0x75, 0x00, 0x00, 0x00, 0x28, 0x0c, 0x42, 0x03, + 0x00, 0x4b, 0x4c, 0x00, 0x00, 0x00, 0x09, 0x4b, + 0x79, 0x00, 0x00, 0x00, 0x28, 0x0c, 0x42, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00, 0x06, - 0x00, 0x7d, 0x01, 0x98, 0x04, 0x01, 0x00, 0x40, - 0xae, 0x04, 0x13, 0x10, 0xbc, 0x03, 0x00, 0x02, - 0xc0, 0x03, 0x01, 0x02, 0x88, 0x04, 0x03, 0x02, - 0x94, 0x04, 0x06, 0x10, 0x96, 0x04, 0x05, 0x10, + 0x00, 0x7d, 0x01, 0xa0, 0x04, 0x01, 0x00, 0x40, + 0xb6, 0x04, 0x13, 0x10, 0xc4, 0x03, 0x00, 0x02, + 0xc8, 0x03, 0x01, 0x02, 0x90, 0x04, 0x03, 0x02, + 0x9c, 0x04, 0x06, 0x10, 0x9e, 0x04, 0x05, 0x10, 0x60, 0x00, 0x00, 0x64, 0x00, 0x00, 0x11, 0xbf, 0xb0, 0xf4, 0x07, 0xc2, 0x65, 0x00, 0x00, 0xf6, 0x44, 0x11, 0xc0, 0xb0, 0xf4, 0x07, 0xc1, 0x65, 0x00, 0x00, 0xf6, 0x39, 0x11, 0xc1, 0xb0, 0xf4, - 0x0c, 0xe8, 0x11, 0x04, 0x1c, 0x01, 0x00, 0x00, + 0x0c, 0xe8, 0x11, 0x04, 0x20, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x11, 0xc2, 0xb0, 0xf4, - 0x13, 0x0b, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x4b, - 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x75, 0x00, - 0x00, 0x00, 0x28, 0xe9, 0x11, 0x04, 0x22, 0x01, + 0x13, 0x0b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x4b, + 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x79, 0x00, + 0x00, 0x00, 0x28, 0xe9, 0x11, 0x04, 0x26, 0x01, 0x00, 0x00, 0x64, 0x00, 0x00, 0x71, 0x02, 0x00, 0x21, 0x01, 0x00, 0x30, 0x0e, 0xea, 0x64, 0x04, 0x00, 0x64, 0x05, 0x00, 0xfa, 0xd3, 0x61, 0x00, 0x00, 0xf4, 0x05, 0x61, 0x00, 0x00, 0x30, 0xc2, - 0x65, 0x00, 0x00, 0x0b, 0x38, 0x4d, 0x00, 0x00, - 0x00, 0x4b, 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, - 0x75, 0x00, 0x00, 0x00, 0x28, 0x60, 0x01, 0x00, + 0x65, 0x00, 0x00, 0x0b, 0x38, 0x51, 0x00, 0x00, + 0x00, 0x4b, 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, + 0x79, 0x00, 0x00, 0x00, 0x28, 0x60, 0x01, 0x00, 0x60, 0x00, 0x00, 0xdb, 0xd3, 0xdc, 0x11, 0xfc, - 0xf4, 0x08, 0x0e, 0x38, 0x4d, 0x00, 0x00, 0x00, + 0xf4, 0x08, 0x0e, 0x38, 0x51, 0x00, 0x00, 0x00, 0xe4, 0xd4, 0x60, 0x14, 0x00, 0x60, 0x13, 0x00, 0x60, 0x08, 0x00, 0x60, 0x07, 0x00, 0x60, 0x06, 0x00, 0x60, 0x05, 0x00, 0x60, 0x04, 0x00, 0x60, 0x03, 0x00, 0x60, 0x02, 0x00, 0x5d, 0x04, 0x00, - 0xdb, 0x04, 0x23, 0x01, 0x00, 0x00, 0xfa, 0x0e, - 0xdc, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xb0, 0xf4, + 0xdb, 0x04, 0x27, 0x01, 0x00, 0x00, 0xfa, 0x0e, + 0xdc, 0x38, 0x51, 0x00, 0x00, 0x00, 0xb0, 0xf4, 0x06, 0x0c, 0x07, 0xe0, 0xf6, 0x0c, 0x5d, 0x04, - 0x00, 0xdc, 0x04, 0x24, 0x01, 0x00, 0x00, 0xfa, - 0x0e, 0xdc, 0x40, 0x10, 0x01, 0x00, 0x00, 0xd5, - 0x61, 0x02, 0x00, 0x38, 0x4d, 0x00, 0x00, 0x00, - 0xb0, 0xf4, 0x09, 0x04, 0x21, 0x01, 0x00, 0x00, - 0x62, 0x02, 0x00, 0x61, 0x02, 0x00, 0x04, 0x1f, + 0x00, 0xdc, 0x04, 0x28, 0x01, 0x00, 0x00, 0xfa, + 0x0e, 0xdc, 0x40, 0x14, 0x01, 0x00, 0x00, 0xd5, + 0x61, 0x02, 0x00, 0x38, 0x51, 0x00, 0x00, 0x00, + 0xb0, 0xf4, 0x09, 0x04, 0x25, 0x01, 0x00, 0x00, + 0x62, 0x02, 0x00, 0x61, 0x02, 0x00, 0x04, 0x23, 0x01, 0x00, 0x00, 0xb0, 0x11, 0xf5, 0x18, 0x0e, - 0x61, 0x02, 0x00, 0x04, 0x1e, 0x01, 0x00, 0x00, + 0x61, 0x02, 0x00, 0x04, 0x22, 0x01, 0x00, 0x00, 0xb0, 0x11, 0xf5, 0x0b, 0x0e, 0x61, 0x02, 0x00, - 0x04, 0x21, 0x01, 0x00, 0x00, 0xb0, 0x98, 0xf4, - 0x0c, 0xe7, 0x11, 0x04, 0x25, 0x01, 0x00, 0x00, - 0x21, 0x01, 0x00, 0x30, 0x38, 0x4d, 0x00, 0x00, - 0x00, 0xd6, 0x61, 0x02, 0x00, 0x04, 0x1e, 0x01, - 0x00, 0x00, 0xb0, 0xf4, 0x22, 0xdc, 0x40, 0x11, + 0x04, 0x25, 0x01, 0x00, 0x00, 0xb0, 0x98, 0xf4, + 0x0c, 0xe7, 0x11, 0x04, 0x29, 0x01, 0x00, 0x00, + 0x21, 0x01, 0x00, 0x30, 0x38, 0x51, 0x00, 0x00, + 0x00, 0xd6, 0x61, 0x02, 0x00, 0x04, 0x22, 0x01, + 0x00, 0x00, 0xb0, 0xf4, 0x22, 0xdc, 0x40, 0x15, 0x01, 0x00, 0x00, 0x62, 0x03, 0x00, 0x61, 0x03, - 0x00, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xb1, 0xf4, + 0x00, 0x38, 0x51, 0x00, 0x00, 0x00, 0xb1, 0xf4, 0x0e, 0x5d, 0x04, 0x00, 0x61, 0x03, 0x00, 0x04, - 0x26, 0x01, 0x00, 0x00, 0xfa, 0x0e, 0x26, 0x00, + 0x2a, 0x01, 0x00, 0x00, 0xfa, 0x0e, 0x26, 0x00, 0x00, 0xcd, 0x04, 0xbf, 0xcd, 0x05, 0x26, 0x00, 0x00, 0xcd, 0x06, 0x26, 0x00, 0x00, 0xcd, 0x07, 0x26, 0x00, 0x00, 0xcd, 0x08, 0x60, 0x09, 0x00, @@ -273,9 +273,9 @@ const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { 0x46, 0xcd, 0x0c, 0x5d, 0x06, 0x00, 0xdb, 0x61, 0x0c, 0x00, 0xfa, 0xf4, 0x74, 0x60, 0x0d, 0x00, 0xdb, 0x61, 0x0c, 0x00, 0x46, 0xcd, 0x0d, 0x61, - 0x0d, 0x00, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xb1, + 0x0d, 0x00, 0x38, 0x51, 0x00, 0x00, 0x00, 0xb1, 0xf4, 0x5f, 0x60, 0x0e, 0x00, 0x5d, 0x04, 0x00, - 0x61, 0x0d, 0x00, 0x04, 0x27, 0x01, 0x00, 0x00, + 0x61, 0x0d, 0x00, 0x04, 0x2b, 0x01, 0x00, 0x00, 0xfa, 0x0e, 0x61, 0x0d, 0x00, 0x5d, 0x07, 0x00, 0x46, 0xcd, 0x0e, 0x61, 0x0e, 0x00, 0xf4, 0x0c, 0xe9, 0x61, 0x0d, 0x00, 0x61, 0x0e, 0x00, 0xfa, @@ -284,7 +284,7 @@ const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { 0x1b, 0x1b, 0x61, 0x0d, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x61, 0x07, 0x00, 0x61, 0x0a, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, - 0x61, 0x0d, 0x00, 0x40, 0x76, 0x00, 0x00, 0x00, + 0x61, 0x0d, 0x00, 0x40, 0x7a, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x09, 0x62, 0x0b, 0x00, 0x61, 0x0b, 0x00, 0xf4, 0x38, 0x60, 0x0f, 0x00, 0x61, 0x0a, 0x00, 0xc0, 0x9f, 0xcd, 0x0f, 0x61, @@ -294,7 +294,7 @@ const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { 0x1b, 0x61, 0x04, 0x00, 0x61, 0x0f, 0x00, 0x46, 0x1b, 0x72, 0x1b, 0x48, 0x93, 0x0f, 0xf6, 0xd8, 0x92, 0x05, 0x92, 0x0a, 0x93, 0x0a, 0xf7, 0x26, - 0xff, 0x61, 0x02, 0x00, 0x04, 0x1e, 0x01, 0x00, + 0xff, 0x61, 0x02, 0x00, 0x04, 0x22, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x62, 0x61, 0x03, 0x00, 0xf4, 0x32, 0x60, 0x10, 0x00, 0xbf, 0xcd, 0x10, 0x61, 0x10, 0x00, 0x61, 0x05, 0x00, 0xa8, 0xf4, 0x4e, @@ -306,13 +306,13 @@ const uint8_t qjsc_builtin_iterator_zip_keyed[2490] = { 0x61, 0x11, 0x00, 0x61, 0x05, 0x00, 0xa8, 0xf4, 0x1d, 0x61, 0x08, 0x00, 0x61, 0x11, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, - 0x38, 0x4d, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, + 0x38, 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x93, 0x11, 0xf6, 0xdc, 0x0e, 0xf6, 0x15, 0xcd, 0x12, 0x6b, 0x10, 0x00, 0x00, 0x00, 0xea, 0x61, 0x06, 0x00, 0x61, 0x05, 0x00, 0xfa, 0x0e, 0xcc, 0x12, 0x30, 0x30, 0xbf, 0xcd, 0x13, 0x61, 0x05, 0x00, 0xcd, 0x14, 0x0b, 0x5d, 0x08, 0x00, - 0x4e, 0xca, 0x00, 0x53, 0x76, 0x00, 0x00, 0x00, + 0x4e, 0xca, 0x00, 0x53, 0x7a, 0x00, 0x00, 0x00, 0x04, 0xca, 0x01, 0x53, 0x06, 0x00, 0x00, 0x00, 0x04, 0x28, 0xca, 0x00, 0xd3, 0xca, 0x01, 0xd4, 0xca, 0x02, 0xd5, 0xca, 0x03, 0x28, 0xca, 0x00, diff --git a/third_party/quickjs/builtin-iterator-zip.h b/third_party/quickjs/builtin-iterator-zip.h index f3f7192..5a2eb49 100644 --- a/third_party/quickjs/builtin-iterator-zip.h +++ b/third_party/quickjs/builtin-iterator-zip.h @@ -5,7 +5,7 @@ const uint32_t qjsc_builtin_iterator_zip_size = 2533; const uint8_t qjsc_builtin_iterator_zip[2533] = { - 0x1b, 0xe6, 0xea, 0xf5, 0x96, 0x2a, 0x01, 0x1c, + 0x1b, 0x7a, 0x89, 0x54, 0xb0, 0x2a, 0x01, 0x1c, 0x49, 0x74, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x48, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x01, 0x08, 0x63, 0x61, 0x6c, 0x6c, 0x01, 0x1e, 0x53, 0x79, @@ -53,95 +53,95 @@ const uint8_t qjsc_builtin_iterator_zip[2533] = { 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x01, 0x18, 0x62, 0x61, 0x64, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x0c, 0x00, 0x02, - 0x00, 0xb0, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x04, 0x01, 0xb2, 0x01, 0x00, 0x00, + 0x00, 0xb8, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x04, 0x01, 0xba, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x43, 0x02, 0x00, 0x00, 0x05, 0x03, - 0x05, 0x01, 0x08, 0x00, 0x04, 0x0c, 0x08, 0xfa, - 0x03, 0x00, 0x01, 0x80, 0x07, 0xc0, 0x03, 0x00, - 0x01, 0x80, 0x03, 0xbc, 0x03, 0x00, 0x01, 0x80, - 0x00, 0xfc, 0x03, 0x00, 0x01, 0x80, 0x01, 0xfe, - 0x03, 0x00, 0x01, 0x80, 0x06, 0x80, 0x04, 0x00, - 0x00, 0x80, 0x05, 0x82, 0x04, 0x00, 0x01, 0x80, - 0x02, 0x84, 0x04, 0x00, 0x02, 0x80, 0x04, 0x0c, - 0x43, 0x02, 0x00, 0x80, 0x04, 0x02, 0x00, 0x02, - 0x03, 0x00, 0x01, 0x00, 0x17, 0x02, 0x86, 0x04, - 0x00, 0x01, 0x00, 0x88, 0x04, 0x00, 0x01, 0x00, - 0xbc, 0x03, 0x02, 0x01, 0xdb, 0x99, 0x04, 0x51, + 0x05, 0x01, 0x08, 0x00, 0x04, 0x0c, 0x08, 0x82, + 0x04, 0x00, 0x01, 0x80, 0x07, 0xc8, 0x03, 0x00, + 0x01, 0x80, 0x03, 0xc4, 0x03, 0x00, 0x01, 0x80, + 0x00, 0x84, 0x04, 0x00, 0x01, 0x80, 0x01, 0x86, + 0x04, 0x00, 0x01, 0x80, 0x06, 0x88, 0x04, 0x00, + 0x00, 0x80, 0x05, 0x8a, 0x04, 0x00, 0x01, 0x80, + 0x02, 0x8c, 0x04, 0x00, 0x02, 0x80, 0x04, 0x0c, + 0x43, 0x02, 0x00, 0x88, 0x04, 0x02, 0x00, 0x02, + 0x03, 0x00, 0x01, 0x00, 0x17, 0x02, 0x8e, 0x04, + 0x00, 0x01, 0x00, 0x90, 0x04, 0x00, 0x01, 0x00, + 0xc4, 0x03, 0x02, 0x01, 0xdb, 0x99, 0x04, 0x55, 0x00, 0x00, 0x00, 0xb0, 0xf4, 0x07, 0xdb, 0x07, 0xb1, 0xf4, 0x02, 0x29, 0xe7, 0x11, 0xdc, 0x21, - 0x01, 0x00, 0x30, 0x0c, 0x43, 0x02, 0x00, 0x82, + 0x01, 0x00, 0x30, 0x0c, 0x43, 0x02, 0x00, 0x8a, 0x04, 0x01, 0x02, 0x01, 0x04, 0x00, 0x01, 0x00, - 0x2e, 0x03, 0x8a, 0x04, 0x00, 0x01, 0x00, 0x8c, - 0x04, 0x02, 0x00, 0x40, 0x8e, 0x04, 0x05, 0x00, - 0x03, 0xfc, 0x03, 0x03, 0x01, 0x6b, 0x23, 0x00, + 0x2e, 0x03, 0x92, 0x04, 0x00, 0x01, 0x00, 0x94, + 0x04, 0x02, 0x00, 0x40, 0x96, 0x04, 0x05, 0x00, + 0x03, 0x84, 0x04, 0x03, 0x01, 0x6b, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xdb, 0x98, 0xf4, 0x04, 0x06, 0x6e, 0x28, 0xdb, 0x40, 0x06, 0x00, 0x00, 0x00, 0xd3, 0x61, 0x00, 0x00, 0xf4, 0x08, 0xe7, 0xdb, 0x61, 0x00, 0x00, 0xfa, 0x0e, 0x0e, 0x29, 0xd4, 0x6b, 0x07, 0x00, 0x00, 0x00, 0xd0, - 0x6e, 0x28, 0x30, 0x0c, 0x43, 0x02, 0x00, 0x84, + 0x6e, 0x28, 0x30, 0x0c, 0x43, 0x02, 0x00, 0x8c, 0x04, 0x02, 0x04, 0x02, 0x03, 0x00, 0x01, 0x00, - 0x53, 0x06, 0x90, 0x04, 0x00, 0x01, 0x00, 0x92, - 0x04, 0x00, 0x01, 0x00, 0x94, 0x04, 0x01, 0x00, - 0x40, 0x96, 0x04, 0x02, 0x01, 0x40, 0x8a, 0x04, - 0x03, 0x02, 0x40, 0x8e, 0x04, 0x03, 0x03, 0x40, - 0x82, 0x04, 0x01, 0x00, 0x60, 0x00, 0x00, 0x38, - 0x4d, 0x00, 0x00, 0x00, 0xd3, 0x60, 0x01, 0x00, + 0x53, 0x06, 0x98, 0x04, 0x00, 0x01, 0x00, 0x9a, + 0x04, 0x00, 0x01, 0x00, 0x9c, 0x04, 0x01, 0x00, + 0x40, 0x9e, 0x04, 0x02, 0x01, 0x40, 0x92, 0x04, + 0x03, 0x02, 0x40, 0x96, 0x04, 0x03, 0x03, 0x40, + 0x8a, 0x04, 0x01, 0x00, 0x60, 0x00, 0x00, 0x38, + 0x51, 0x00, 0x00, 0x00, 0xd3, 0x60, 0x01, 0x00, 0xdc, 0xd4, 0x61, 0x01, 0x00, 0x90, 0x62, 0x01, 0x00, 0xbf, 0xaa, 0xf4, 0x37, 0x60, 0x03, 0x00, 0x60, 0x02, 0x00, 0xdb, 0x61, 0x01, 0x00, 0x46, 0xd5, 0xdb, 0x61, 0x01, 0x00, 0x1b, 0x11, 0xb4, - 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x38, 0x4d, + 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0xe7, 0x61, 0x02, 0x00, 0xf9, 0xd6, 0x61, 0x00, 0x00, 0x98, 0xf4, 0xc8, 0x61, 0x03, 0x00, 0x62, 0x00, 0x00, 0xf6, 0xc0, 0x61, 0x00, 0x00, 0x28, 0x0c, - 0x41, 0x02, 0x00, 0xc0, 0x02, 0x02, 0x1a, 0x01, - 0x05, 0x07, 0x08, 0x02, 0xe2, 0x05, 0x1c, 0x98, - 0x04, 0x00, 0x01, 0x00, 0x9a, 0x04, 0x00, 0x01, - 0x00, 0x98, 0x04, 0x01, 0xff, 0xff, 0xff, 0xff, - 0x0f, 0x40, 0x9a, 0x04, 0x01, 0x01, 0x40, 0x9c, - 0x04, 0x02, 0x00, 0xc0, 0x03, 0x9e, 0x04, 0x02, - 0x03, 0x40, 0xa0, 0x04, 0x02, 0x04, 0xc0, 0x04, - 0x90, 0x04, 0x02, 0x05, 0xc0, 0x02, 0xa2, 0x04, - 0x02, 0x06, 0xc0, 0x05, 0x92, 0x04, 0x02, 0x07, - 0xc0, 0x01, 0xa4, 0x04, 0x02, 0x08, 0x40, 0xa6, - 0x04, 0x02, 0x09, 0x40, 0xec, 0x01, 0x09, 0x1a, - 0x40, 0xa8, 0x04, 0x0b, 0x0b, 0x40, 0x8e, 0x04, - 0x0d, 0x0f, 0x03, 0x8a, 0x04, 0x0b, 0x0c, 0x40, - 0x8c, 0x04, 0x0b, 0x0e, 0x40, 0xec, 0x01, 0x13, - 0x0b, 0x40, 0x96, 0x04, 0x13, 0x10, 0x40, 0xea, - 0x01, 0x13, 0x11, 0x40, 0x90, 0x01, 0x15, 0x16, - 0x40, 0xa8, 0x04, 0x16, 0x13, 0x40, 0x8e, 0x04, - 0x17, 0x13, 0x03, 0xaa, 0x04, 0x13, 0x12, 0x40, - 0x94, 0x04, 0x1c, 0x16, 0x40, 0x8e, 0x04, 0x1f, - 0x1a, 0x03, 0xac, 0x04, 0x02, 0x0a, 0xc0, 0x00, - 0xae, 0x04, 0x02, 0x19, 0xc0, 0x06, 0xbc, 0x03, - 0x02, 0x01, 0xc0, 0x03, 0x01, 0x01, 0xfc, 0x03, - 0x03, 0x01, 0x84, 0x04, 0x02, 0x00, 0x80, 0x04, - 0x00, 0x00, 0xfe, 0x03, 0x04, 0x01, 0x82, 0x04, - 0x01, 0x00, 0xfa, 0x03, 0x00, 0x01, 0x0c, 0x42, + 0x41, 0x02, 0x00, 0xc8, 0x02, 0x02, 0x1a, 0x01, + 0x05, 0x07, 0x08, 0x02, 0xe2, 0x05, 0x1c, 0xa0, + 0x04, 0x00, 0x01, 0x00, 0xa2, 0x04, 0x00, 0x01, + 0x00, 0xa0, 0x04, 0x01, 0xff, 0xff, 0xff, 0xff, + 0x0f, 0x40, 0xa2, 0x04, 0x01, 0x01, 0x40, 0xa4, + 0x04, 0x02, 0x00, 0xc0, 0x03, 0xa6, 0x04, 0x02, + 0x03, 0x40, 0xa8, 0x04, 0x02, 0x04, 0xc0, 0x04, + 0x98, 0x04, 0x02, 0x05, 0xc0, 0x02, 0xaa, 0x04, + 0x02, 0x06, 0xc0, 0x05, 0x9a, 0x04, 0x02, 0x07, + 0xc0, 0x01, 0xac, 0x04, 0x02, 0x08, 0x40, 0xae, + 0x04, 0x02, 0x09, 0x40, 0xf4, 0x01, 0x09, 0x1a, + 0x40, 0xb0, 0x04, 0x0b, 0x0b, 0x40, 0x96, 0x04, + 0x0d, 0x0f, 0x03, 0x92, 0x04, 0x0b, 0x0c, 0x40, + 0x94, 0x04, 0x0b, 0x0e, 0x40, 0xf4, 0x01, 0x13, + 0x0b, 0x40, 0x9e, 0x04, 0x13, 0x10, 0x40, 0xf2, + 0x01, 0x13, 0x11, 0x40, 0x98, 0x01, 0x15, 0x16, + 0x40, 0xb0, 0x04, 0x16, 0x13, 0x40, 0x96, 0x04, + 0x17, 0x13, 0x03, 0xb2, 0x04, 0x13, 0x12, 0x40, + 0x9c, 0x04, 0x1c, 0x16, 0x40, 0x96, 0x04, 0x1f, + 0x1a, 0x03, 0xb4, 0x04, 0x02, 0x0a, 0xc0, 0x00, + 0xb6, 0x04, 0x02, 0x19, 0xc0, 0x06, 0xc4, 0x03, + 0x02, 0x01, 0xc8, 0x03, 0x01, 0x01, 0x84, 0x04, + 0x03, 0x01, 0x8c, 0x04, 0x02, 0x00, 0x88, 0x04, + 0x00, 0x00, 0x86, 0x04, 0x04, 0x01, 0x8a, 0x04, + 0x01, 0x00, 0x82, 0x04, 0x00, 0x01, 0x0c, 0x42, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, - 0x0b, 0x00, 0xca, 0x04, 0x08, 0xb0, 0x04, 0x01, - 0x00, 0x40, 0xee, 0x01, 0x01, 0x01, 0x40, 0xb2, - 0x04, 0x01, 0x02, 0x40, 0x96, 0x04, 0x03, 0x03, - 0x40, 0x8a, 0x04, 0x04, 0x04, 0x40, 0xb4, 0x04, - 0x04, 0x05, 0x40, 0x8e, 0x04, 0x09, 0x06, 0x03, - 0x94, 0x04, 0x10, 0x06, 0x40, 0xac, 0x04, 0x18, - 0x10, 0xbc, 0x03, 0x00, 0x02, 0xc0, 0x03, 0x01, - 0x02, 0x92, 0x04, 0x07, 0x10, 0x90, 0x04, 0x05, - 0x10, 0x9c, 0x04, 0x02, 0x10, 0xa0, 0x04, 0x04, - 0x10, 0xfc, 0x03, 0x02, 0x02, 0xa2, 0x04, 0x06, - 0x10, 0xae, 0x04, 0x19, 0x10, 0x84, 0x04, 0x03, + 0x0b, 0x00, 0xca, 0x04, 0x08, 0xb8, 0x04, 0x01, + 0x00, 0x40, 0xf6, 0x01, 0x01, 0x01, 0x40, 0xba, + 0x04, 0x01, 0x02, 0x40, 0x9e, 0x04, 0x03, 0x03, + 0x40, 0x92, 0x04, 0x04, 0x04, 0x40, 0xbc, 0x04, + 0x04, 0x05, 0x40, 0x96, 0x04, 0x09, 0x06, 0x03, + 0x9c, 0x04, 0x10, 0x06, 0x40, 0xb4, 0x04, 0x18, + 0x10, 0xc4, 0x03, 0x00, 0x02, 0xc8, 0x03, 0x01, + 0x02, 0x9a, 0x04, 0x07, 0x10, 0x98, 0x04, 0x05, + 0x10, 0xa4, 0x04, 0x02, 0x10, 0xa8, 0x04, 0x04, + 0x10, 0x84, 0x04, 0x02, 0x02, 0xaa, 0x04, 0x06, + 0x10, 0xb6, 0x04, 0x19, 0x10, 0x8c, 0x04, 0x03, 0x02, 0x60, 0x02, 0x00, 0x60, 0x01, 0x00, 0x60, 0x00, 0x00, 0x64, 0x00, 0x00, 0x11, 0xbf, 0xb0, 0xf5, 0x06, 0x11, 0xc0, 0xb0, 0xf4, 0x07, 0xc1, 0x65, 0x00, 0x00, 0xf6, 0x33, 0x11, 0xc1, 0xb0, - 0xf4, 0x0c, 0xe8, 0x11, 0x04, 0x1b, 0x01, 0x00, + 0xf4, 0x0c, 0xe8, 0x11, 0x04, 0x1f, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x11, 0xc2, 0xb0, - 0xf4, 0x13, 0x0b, 0x38, 0x4d, 0x00, 0x00, 0x00, - 0x4b, 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x75, - 0x00, 0x00, 0x00, 0x28, 0xe9, 0x11, 0x04, 0x1c, + 0xf4, 0x13, 0x0b, 0x38, 0x51, 0x00, 0x00, 0x00, + 0x4b, 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x79, + 0x00, 0x00, 0x00, 0x28, 0xe9, 0x11, 0x04, 0x20, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x0e, 0xbf, 0xd3, 0xbf, 0xd4, 0x26, 0x00, 0x00, 0xd5, 0x60, 0x03, 0x00, 0xbf, 0xd6, 0x61, 0x03, 0x00, @@ -149,8 +149,8 @@ const uint8_t qjsc_builtin_iterator_zip[2533] = { 0x00, 0x60, 0x05, 0x00, 0x60, 0x04, 0x00, 0x64, 0x04, 0x00, 0x61, 0x03, 0x00, 0x46, 0xcd, 0x04, 0x61, 0x04, 0x00, 0x98, 0xf4, 0x34, 0x64, 0x05, - 0x00, 0x04, 0x1d, 0x01, 0x00, 0x00, 0xb1, 0xf4, - 0x0c, 0xe9, 0x11, 0x04, 0x1c, 0x01, 0x00, 0x00, + 0x00, 0x04, 0x21, 0x01, 0x00, 0x00, 0xb1, 0xf4, + 0x0c, 0xe9, 0x11, 0x04, 0x20, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x61, 0x02, 0x00, 0x61, 0x03, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x64, 0x06, 0x00, 0x61, 0x03, @@ -162,115 +162,115 @@ const uint8_t qjsc_builtin_iterator_zip[2533] = { 0x2e, 0x00, 0x00, 0x00, 0xbf, 0x65, 0x09, 0x00, 0x64, 0x04, 0x00, 0x61, 0x03, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x38, - 0x4d, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, + 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x5d, 0x0a, 0x00, 0x64, 0x04, 0x00, 0x64, 0x03, 0x00, 0xfa, 0x0e, 0xcc, 0x06, 0x30, 0x30, 0x61, - 0x05, 0x00, 0x40, 0x75, 0x00, 0x00, 0x00, 0x98, - 0xf4, 0x48, 0x64, 0x05, 0x00, 0x04, 0x1e, 0x01, + 0x05, 0x00, 0x40, 0x79, 0x00, 0x00, 0x00, 0x98, + 0xf4, 0x48, 0x64, 0x05, 0x00, 0x04, 0x22, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x1d, 0xbf, 0xb2, 0x00, 0x02, 0xf4, 0x17, 0x5d, 0x0a, 0x00, 0x64, 0x04, 0x00, 0x64, 0x03, 0x00, 0xfa, 0x0e, 0xe8, 0x11, - 0x04, 0x1f, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, + 0x04, 0x23, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x61, 0x02, 0x00, 0x61, 0x03, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, - 0x61, 0x05, 0x00, 0x40, 0x48, 0x00, 0x00, 0x00, + 0x61, 0x05, 0x00, 0x40, 0x4c, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x93, 0x01, 0xf7, 0xc6, 0x00, 0x64, 0x09, 0x00, 0x90, 0x65, 0x09, 0x00, 0x0e, 0x93, 0x00, 0x64, 0x04, 0x00, 0x61, 0x03, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, - 0x1b, 0x1b, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x1b, + 0x1b, 0x1b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x64, 0x05, 0x00, 0x60, 0x07, - 0x00, 0x11, 0x04, 0x20, 0x01, 0x00, 0x00, 0xb0, + 0x00, 0x11, 0x04, 0x24, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x2c, 0x5d, 0x0a, 0x00, 0x64, 0x04, 0x00, 0x64, 0x03, 0x00, 0xfa, 0xcd, 0x07, 0x61, 0x07, 0x00, 0xf4, 0x05, 0x61, 0x07, 0x00, 0x30, 0xc2, - 0x65, 0x00, 0x00, 0x0b, 0x38, 0x4d, 0x00, 0x00, - 0x00, 0x4b, 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, - 0x75, 0x00, 0x00, 0x00, 0x28, 0x11, 0x04, 0x1d, + 0x65, 0x00, 0x00, 0x0b, 0x38, 0x51, 0x00, 0x00, + 0x00, 0x4b, 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, + 0x79, 0x00, 0x00, 0x00, 0x28, 0x11, 0x04, 0x21, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x3a, 0x64, 0x09, 0x00, 0xc0, 0xa8, 0xf4, 0x17, 0xc2, 0x65, 0x00, - 0x00, 0x0b, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x4b, - 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x75, 0x00, + 0x00, 0x0b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x4b, + 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x79, 0x00, 0x00, 0x00, 0x28, 0x61, 0x02, 0x00, 0x61, 0x03, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x64, 0x06, 0x00, 0x61, 0x03, 0x00, 0x46, 0x1b, 0x72, 0x1b, 0x48, 0xf6, 0x26, 0x11, - 0x04, 0x1e, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x1d, + 0x04, 0x22, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x1d, 0xbf, 0xb2, 0x01, 0x02, 0xf4, 0x17, 0x5d, 0x0a, 0x00, 0x64, 0x04, 0x00, 0x64, 0x03, 0x00, 0xfa, - 0x0e, 0xe8, 0x11, 0x04, 0x1f, 0x01, 0x00, 0x00, + 0x0e, 0xe8, 0x11, 0x04, 0x23, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x0e, 0x93, 0x03, 0xf7, 0x45, 0xfe, 0x61, 0x01, 0x00, 0xbf, 0xb0, 0xf4, - 0x17, 0xc2, 0x65, 0x00, 0x00, 0x0b, 0x38, 0x4d, - 0x00, 0x00, 0x00, 0x4b, 0x48, 0x00, 0x00, 0x00, - 0x0a, 0x4b, 0x75, 0x00, 0x00, 0x00, 0x28, 0xc0, + 0x17, 0xc2, 0x65, 0x00, 0x00, 0x0b, 0x38, 0x51, + 0x00, 0x00, 0x00, 0x4b, 0x4c, 0x00, 0x00, 0x00, + 0x0a, 0x4b, 0x79, 0x00, 0x00, 0x00, 0x28, 0xc0, 0x65, 0x00, 0x00, 0x0b, 0x61, 0x02, 0x00, 0x4b, - 0x48, 0x00, 0x00, 0x00, 0x09, 0x4b, 0x75, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0x09, 0x4b, 0x79, 0x00, 0x00, 0x00, 0x28, 0x0c, 0x42, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00, 0x06, 0x00, 0x7d, - 0x01, 0x94, 0x04, 0x01, 0x00, 0x40, 0xac, 0x04, - 0x18, 0x10, 0xbc, 0x03, 0x00, 0x02, 0xc0, 0x03, - 0x01, 0x02, 0x84, 0x04, 0x03, 0x02, 0x90, 0x04, - 0x05, 0x10, 0x92, 0x04, 0x07, 0x10, 0x60, 0x00, + 0x01, 0x9c, 0x04, 0x01, 0x00, 0x40, 0xb4, 0x04, + 0x18, 0x10, 0xc4, 0x03, 0x00, 0x02, 0xc8, 0x03, + 0x01, 0x02, 0x8c, 0x04, 0x03, 0x02, 0x98, 0x04, + 0x05, 0x10, 0x9a, 0x04, 0x07, 0x10, 0x60, 0x00, 0x00, 0x64, 0x00, 0x00, 0x11, 0xbf, 0xb0, 0xf4, 0x07, 0xc2, 0x65, 0x00, 0x00, 0xf6, 0x44, 0x11, 0xc0, 0xb0, 0xf4, 0x07, 0xc1, 0x65, 0x00, 0x00, 0xf6, 0x39, 0x11, 0xc1, 0xb0, 0xf4, 0x0c, 0xe8, - 0x11, 0x04, 0x1b, 0x01, 0x00, 0x00, 0x21, 0x01, + 0x11, 0x04, 0x1f, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x11, 0xc2, 0xb0, 0xf4, 0x13, 0x0b, - 0x38, 0x4d, 0x00, 0x00, 0x00, 0x4b, 0x48, 0x00, - 0x00, 0x00, 0x0a, 0x4b, 0x75, 0x00, 0x00, 0x00, - 0x28, 0xe9, 0x11, 0x04, 0x21, 0x01, 0x00, 0x00, + 0x38, 0x51, 0x00, 0x00, 0x00, 0x4b, 0x4c, 0x00, + 0x00, 0x00, 0x0a, 0x4b, 0x79, 0x00, 0x00, 0x00, + 0x28, 0xe9, 0x11, 0x04, 0x25, 0x01, 0x00, 0x00, 0x64, 0x00, 0x00, 0x71, 0x02, 0x00, 0x21, 0x01, 0x00, 0x30, 0x0e, 0xea, 0x64, 0x04, 0x00, 0x64, 0x05, 0x00, 0xfa, 0xd3, 0x61, 0x00, 0x00, 0xf4, 0x05, 0x61, 0x00, 0x00, 0x30, 0xc2, 0x65, 0x00, - 0x00, 0x0b, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x4b, - 0x48, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x75, 0x00, + 0x00, 0x0b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x4b, + 0x4c, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x79, 0x00, 0x00, 0x00, 0x28, 0x60, 0x01, 0x00, 0x60, 0x00, 0x00, 0xdb, 0xd3, 0xdc, 0x11, 0xfc, 0xf4, 0x08, - 0x0e, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xe4, 0xd4, + 0x0e, 0x38, 0x51, 0x00, 0x00, 0x00, 0xe4, 0xd4, 0x60, 0x19, 0x00, 0x60, 0x18, 0x00, 0x60, 0x09, 0x00, 0x60, 0x08, 0x00, 0x60, 0x07, 0x00, 0x60, 0x06, 0x00, 0x60, 0x05, 0x00, 0x60, 0x04, 0x00, 0x60, 0x03, 0x00, 0x60, 0x02, 0x00, 0x5d, 0x04, - 0x00, 0xdb, 0x04, 0x22, 0x01, 0x00, 0x00, 0xfa, - 0x0e, 0xdc, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xb0, + 0x00, 0xdb, 0x04, 0x26, 0x01, 0x00, 0x00, 0xfa, + 0x0e, 0xdc, 0x38, 0x51, 0x00, 0x00, 0x00, 0xb0, 0xf4, 0x06, 0x0c, 0x07, 0xe0, 0xf6, 0x0c, 0x5d, - 0x04, 0x00, 0xdc, 0x04, 0x23, 0x01, 0x00, 0x00, - 0xfa, 0x0e, 0xdc, 0x40, 0x0e, 0x01, 0x00, 0x00, - 0xd5, 0x61, 0x02, 0x00, 0x38, 0x4d, 0x00, 0x00, - 0x00, 0xb0, 0xf4, 0x09, 0x04, 0x20, 0x01, 0x00, + 0x04, 0x00, 0xdc, 0x04, 0x27, 0x01, 0x00, 0x00, + 0xfa, 0x0e, 0xdc, 0x40, 0x12, 0x01, 0x00, 0x00, + 0xd5, 0x61, 0x02, 0x00, 0x38, 0x51, 0x00, 0x00, + 0x00, 0xb0, 0xf4, 0x09, 0x04, 0x24, 0x01, 0x00, 0x00, 0x62, 0x02, 0x00, 0x61, 0x02, 0x00, 0x04, - 0x1e, 0x01, 0x00, 0x00, 0xb0, 0x11, 0xf5, 0x18, - 0x0e, 0x61, 0x02, 0x00, 0x04, 0x1d, 0x01, 0x00, + 0x22, 0x01, 0x00, 0x00, 0xb0, 0x11, 0xf5, 0x18, + 0x0e, 0x61, 0x02, 0x00, 0x04, 0x21, 0x01, 0x00, 0x00, 0xb0, 0x11, 0xf5, 0x0b, 0x0e, 0x61, 0x02, - 0x00, 0x04, 0x20, 0x01, 0x00, 0x00, 0xb0, 0x98, - 0xf4, 0x0c, 0xe7, 0x11, 0x04, 0x24, 0x01, 0x00, - 0x00, 0x21, 0x01, 0x00, 0x30, 0x38, 0x4d, 0x00, - 0x00, 0x00, 0xd6, 0x61, 0x02, 0x00, 0x04, 0x1d, + 0x00, 0x04, 0x24, 0x01, 0x00, 0x00, 0xb0, 0x98, + 0xf4, 0x0c, 0xe7, 0x11, 0x04, 0x28, 0x01, 0x00, + 0x00, 0x21, 0x01, 0x00, 0x30, 0x38, 0x51, 0x00, + 0x00, 0x00, 0xd6, 0x61, 0x02, 0x00, 0x04, 0x21, 0x01, 0x00, 0x00, 0xb0, 0xf4, 0x22, 0xdc, 0x40, - 0x0f, 0x01, 0x00, 0x00, 0x62, 0x03, 0x00, 0x61, - 0x03, 0x00, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xb1, + 0x13, 0x01, 0x00, 0x00, 0x62, 0x03, 0x00, 0x61, + 0x03, 0x00, 0x38, 0x51, 0x00, 0x00, 0x00, 0xb1, 0xf4, 0x0e, 0x5d, 0x04, 0x00, 0x61, 0x03, 0x00, - 0x04, 0x25, 0x01, 0x00, 0x00, 0xfa, 0x0e, 0x26, + 0x04, 0x29, 0x01, 0x00, 0x00, 0xfa, 0x0e, 0x26, 0x00, 0x00, 0xcd, 0x04, 0x26, 0x00, 0x00, 0xcd, 0x05, 0x26, 0x00, 0x00, 0xcd, 0x06, 0xbf, 0xcd, - 0x07, 0x38, 0x4d, 0x00, 0x00, 0x00, 0xcd, 0x08, + 0x07, 0x38, 0x51, 0x00, 0x00, 0x00, 0xcd, 0x08, 0xdb, 0x5d, 0x05, 0x00, 0x47, 0x24, 0x00, 0x00, 0xcd, 0x09, 0x6b, 0xa8, 0x01, 0x00, 0x00, 0x60, - 0x0a, 0x00, 0x61, 0x09, 0x00, 0x40, 0x76, 0x00, + 0x0a, 0x00, 0x61, 0x09, 0x00, 0x40, 0x7a, 0x00, 0x00, 0x00, 0xcd, 0x0a, 0x60, 0x0e, 0x00, 0x60, 0x0d, 0x00, 0x60, 0x0b, 0x00, 0x06, 0xcd, 0x0b, 0x6b, 0x12, 0x00, 0x00, 0x00, 0xe9, 0x61, 0x09, 0x00, 0x61, 0x0a, 0x00, 0xfa, 0x62, 0x0b, 0x00, 0x0e, 0xf6, 0x14, 0xcd, 0x0c, 0x6b, 0x0f, 0x00, - 0x00, 0x00, 0x38, 0x4d, 0x00, 0x00, 0x00, 0x62, + 0x00, 0x00, 0x38, 0x51, 0x00, 0x00, 0x00, 0x62, 0x09, 0x00, 0xcc, 0x0c, 0x30, 0x30, 0x61, 0x0b, - 0x00, 0x40, 0x75, 0x00, 0x00, 0x00, 0xf5, 0x67, - 0x61, 0x0b, 0x00, 0x40, 0x48, 0x00, 0x00, 0x00, + 0x00, 0x40, 0x79, 0x00, 0x00, 0x00, 0xf5, 0x67, + 0x61, 0x0b, 0x00, 0x40, 0x4c, 0x00, 0x00, 0x00, 0xcd, 0x0d, 0x5d, 0x04, 0x00, 0x61, 0x0d, 0x00, - 0x04, 0x26, 0x01, 0x00, 0x00, 0xfa, 0x0e, 0x61, + 0x04, 0x2a, 0x01, 0x00, 0x00, 0xfa, 0x0e, 0x61, 0x0d, 0x00, 0x5d, 0x05, 0x00, 0x46, 0xcd, 0x0e, 0x61, 0x0e, 0x00, 0xf4, 0x0c, 0xe9, 0x61, 0x0d, 0x00, 0x61, 0x0e, 0x00, 0xfa, 0x62, 0x0d, 0x00, @@ -279,37 +279,37 @@ const uint8_t qjsc_builtin_iterator_zip[2533] = { 0x0d, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x61, 0x06, 0x00, 0x61, 0x07, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x61, 0x0d, 0x00, - 0x40, 0x76, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, - 0x48, 0x93, 0x07, 0xf7, 0x60, 0xff, 0x38, 0x4d, + 0x40, 0x7a, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, + 0x48, 0x93, 0x07, 0xf7, 0x60, 0xff, 0x38, 0x51, 0x00, 0x00, 0x00, 0x62, 0x09, 0x00, 0x61, 0x03, 0x00, 0x68, 0xe6, 0x00, 0x00, 0x00, 0x60, 0x15, 0x00, 0x60, 0x11, 0x00, 0x60, 0x10, 0x00, 0x60, 0x0f, 0x00, 0x61, 0x03, 0x00, 0x5d, 0x05, 0x00, 0x47, 0x24, 0x00, 0x00, 0x62, 0x08, 0x00, 0x61, - 0x08, 0x00, 0x40, 0x76, 0x00, 0x00, 0x00, 0xcd, + 0x08, 0x00, 0x40, 0x7a, 0x00, 0x00, 0x00, 0xcd, 0x0f, 0xbf, 0xcd, 0x10, 0x09, 0xcd, 0x11, 0x61, 0x10, 0x00, 0x61, 0x07, 0x00, 0xa8, 0xf4, 0x64, 0x60, 0x12, 0x00, 0x06, 0xcd, 0x12, 0x6b, 0x2a, 0x00, 0x00, 0x00, 0x60, 0x13, 0x00, 0xe9, 0x61, 0x08, 0x00, 0x61, 0x0f, 0x00, 0xfa, 0xcd, 0x13, - 0x61, 0x13, 0x00, 0x40, 0x75, 0x00, 0x00, 0x00, - 0x62, 0x11, 0x00, 0x61, 0x13, 0x00, 0x40, 0x48, + 0x61, 0x13, 0x00, 0x40, 0x79, 0x00, 0x00, 0x00, + 0x62, 0x11, 0x00, 0x61, 0x13, 0x00, 0x40, 0x4c, 0x00, 0x00, 0x00, 0x62, 0x12, 0x00, 0x0e, 0xf6, 0x14, 0xcd, 0x14, 0x6b, 0x0f, 0x00, 0x00, 0x00, - 0x38, 0x4d, 0x00, 0x00, 0x00, 0x62, 0x08, 0x00, + 0x38, 0x51, 0x00, 0x00, 0x00, 0x62, 0x08, 0x00, 0xcc, 0x14, 0x30, 0x30, 0x61, 0x11, 0x00, 0xf5, 0x1b, 0x61, 0x04, 0x00, 0x61, 0x10, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, 0x72, 0x1b, 0x1b, 0x61, 0x12, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x93, 0x10, 0xf6, 0x95, 0x61, 0x08, 0x00, 0xcd, 0x15, - 0x38, 0x4d, 0x00, 0x00, 0x00, 0x62, 0x08, 0x00, + 0x38, 0x51, 0x00, 0x00, 0x00, 0x62, 0x08, 0x00, 0x61, 0x11, 0x00, 0x98, 0xf4, 0x16, 0x60, 0x16, 0x00, 0x5d, 0x06, 0x00, 0x61, 0x15, 0x00, 0xf9, 0xcd, 0x16, 0x61, 0x16, 0x00, 0xf4, 0x05, 0x61, 0x16, 0x00, 0x30, 0x61, 0x10, 0x00, 0x61, 0x07, 0x00, 0xa8, 0xf4, 0x1d, 0x61, 0x04, 0x00, 0x61, 0x10, 0x00, 0x1b, 0x11, 0xb4, 0xf5, 0x04, 0x1b, - 0x72, 0x1b, 0x1b, 0x38, 0x4d, 0x00, 0x00, 0x00, + 0x72, 0x1b, 0x1b, 0x38, 0x51, 0x00, 0x00, 0x00, 0x1b, 0x72, 0x1b, 0x48, 0x93, 0x10, 0xf6, 0xdc, 0x0e, 0xf6, 0x25, 0xcd, 0x17, 0x6b, 0x20, 0x00, 0x00, 0x00, 0xea, 0x61, 0x05, 0x00, 0x61, 0x07, @@ -317,7 +317,7 @@ const uint8_t qjsc_builtin_iterator_zip[2533] = { 0x00, 0xf9, 0x0e, 0x5d, 0x06, 0x00, 0x61, 0x08, 0x00, 0xf9, 0x0e, 0xcc, 0x17, 0x30, 0x30, 0xbf, 0xcd, 0x18, 0x61, 0x07, 0x00, 0xcd, 0x19, 0x0b, - 0x5d, 0x07, 0x00, 0x4e, 0xca, 0x00, 0x53, 0x76, + 0x5d, 0x07, 0x00, 0x4e, 0xca, 0x00, 0x53, 0x7a, 0x00, 0x00, 0x00, 0x04, 0xca, 0x01, 0x53, 0x06, 0x00, 0x00, 0x00, 0x04, 0x28, 0xca, 0x00, 0xd3, 0xca, 0x01, 0xd4, 0xca, 0x02, 0xd5, 0xca, 0x03, diff --git a/third_party/quickjs/dtoa.c b/third_party/quickjs/dtoa.c index a89e824..5840143 100644 --- a/third_party/quickjs/dtoa.c +++ b/third_party/quickjs/dtoa.c @@ -200,7 +200,7 @@ static no_inline limb_t mp_div1norm(limb_t *tabr, const limb_t *taba, limb_t n, static __maybe_unused void mpb_dump(const char *str, const mpb_t *a) { int i; - + printf("%s= 0x", str); for(i = a->len - 1; i >= 0; i--) { printf("%08x", a->tab[i]); @@ -299,7 +299,7 @@ enum { static int mpb_get_bit(const mpb_t *r, int k) { int l; - + l = (unsigned)k / LIMB_BITS; k = k & (LIMB_BITS - 1); if (l >= r->len) @@ -511,7 +511,7 @@ static void build_mul_log2_radix_table(void) static void mul_log2_radix_test(void) { int radix, i, ref, r; - + for(radix = 2; radix <= 36; radix++) { for(i = -2048; i <= 2047; i++) { ref = (int)floor((double)i / log2(radix)); @@ -590,7 +590,7 @@ size_t u32toa(char *buf, uint32_t n) { char buf1[10], *q; size_t len; - + q = buf1 + sizeof(buf1); do { *--q = n % 10 + '0'; @@ -753,7 +753,7 @@ static const int16_t min_exponent[JS_RADIX_MAX - 1] = { void build_tables(void) { int r, j, radix, n, col, i; - + /* radix_base_table */ for(radix = 2; radix <= 36; radix++) { r = 1; @@ -1103,6 +1103,316 @@ static void dtoa_free(void *ptr) } #endif +/* + * Grisu3 fast path for the overwhelmingly common Number::toString case. + * + * This is deliberately kept as a leaf which only returns a decimal mantissa + * and exponent. js_dtoa's existing output code still decides between fixed + * and exponential notation, and the exact bignum implementation below remains + * the fallback for every other radix/format and whenever the fast path cannot + * prove its candidate. The algorithm is from Florian Loitsch, "Printing + * Floating-Point Numbers Quickly and Accurately with Integers" (PLDI 2010). + */ +typedef struct { + uint64_t f; + int e; +} DtoaDiyFp; + +static DtoaDiyFp dtoa_diy_mul(DtoaDiyFp a, DtoaDiyFp b) +{ +#if defined(__SIZEOF_INT128__) + __extension__ typedef unsigned __int128 dtoa_uint128_t; + dtoa_uint128_t p = (dtoa_uint128_t)a.f * b.f; + uint64_t high = p >> 64; + DtoaDiyFp r; + + if ((uint64_t)p & (UINT64_C(1) << 63)) + high++; + r.f = high; + r.e = a.e + b.e + 64; + return r; +#else + uint64_t a_hi = a.f >> 32, a_lo = (uint32_t)a.f; + uint64_t b_hi = b.f >> 32, b_lo = (uint32_t)b.f; + uint64_t ac = a_hi * b_hi; + uint64_t bc = a_lo * b_hi; + uint64_t ad = a_hi * b_lo; + uint64_t bd = a_lo * b_lo; + uint64_t t = (bd >> 32) + (uint32_t)ad + (uint32_t)bc; + DtoaDiyFp r; + + /* Round the discarded low half, exactly as a 128-bit multiply would. */ + t += (uint64_t)1 << 31; + r.f = ac + (ad >> 32) + (bc >> 32) + (t >> 32); + r.e = a.e + b.e + 64; + return r; +#endif +} + +static DtoaDiyFp dtoa_diy_normalize(DtoaDiyFp v) +{ + int n = clz64(v.f); + v.f <<= n; + v.e -= n; + return v; +} + +/* 10^-348, 10^-340, ... 10^340, rounded to a 64-bit significand. */ +static const DtoaDiyFp dtoa_cached_powers[] = { + { UINT64_C(0xfa8fd5a0081c0288), -1220 }, + { UINT64_C(0xbaaee17fa23ebf76), -1193 }, + { UINT64_C(0x8b16fb203055ac76), -1166 }, + { UINT64_C(0xcf42894a5dce35ea), -1140 }, + { UINT64_C(0x9a6bb0aa55653b2d), -1113 }, + { UINT64_C(0xe61acf033d1a45df), -1087 }, + { UINT64_C(0xab70fe17c79ac6ca), -1060 }, + { UINT64_C(0xff77b1fcbebcdc4f), -1034 }, + { UINT64_C(0xbe5691ef416bd60c), -1007 }, + { UINT64_C(0x8dd01fad907ffc3c), -980 }, + { UINT64_C(0xd3515c2831559a83), -954 }, + { UINT64_C(0x9d71ac8fada6c9b5), -927 }, + { UINT64_C(0xea9c227723ee8bcb), -901 }, + { UINT64_C(0xaecc49914078536d), -874 }, + { UINT64_C(0x823c12795db6ce57), -847 }, + { UINT64_C(0xc21094364dfb5637), -821 }, + { UINT64_C(0x9096ea6f3848984f), -794 }, + { UINT64_C(0xd77485cb25823ac7), -768 }, + { UINT64_C(0xa086cfcd97bf97f4), -741 }, + { UINT64_C(0xef340a98172aace5), -715 }, + { UINT64_C(0xb23867fb2a35b28e), -688 }, + { UINT64_C(0x84c8d4dfd2c63f3b), -661 }, + { UINT64_C(0xc5dd44271ad3cdba), -635 }, + { UINT64_C(0x936b9fcebb25c996), -608 }, + { UINT64_C(0xdbac6c247d62a584), -582 }, + { UINT64_C(0xa3ab66580d5fdaf6), -555 }, + { UINT64_C(0xf3e2f893dec3f126), -529 }, + { UINT64_C(0xb5b5ada8aaff80b8), -502 }, + { UINT64_C(0x87625f056c7c4a8b), -475 }, + { UINT64_C(0xc9bcff6034c13053), -449 }, + { UINT64_C(0x964e858c91ba2655), -422 }, + { UINT64_C(0xdff9772470297ebd), -396 }, + { UINT64_C(0xa6dfbd9fb8e5b88f), -369 }, + { UINT64_C(0xf8a95fcf88747d94), -343 }, + { UINT64_C(0xb94470938fa89bcf), -316 }, + { UINT64_C(0x8a08f0f8bf0f156b), -289 }, + { UINT64_C(0xcdb02555653131b6), -263 }, + { UINT64_C(0x993fe2c6d07b7fac), -236 }, + { UINT64_C(0xe45c10c42a2b3b06), -210 }, + { UINT64_C(0xaa242499697392d3), -183 }, + { UINT64_C(0xfd87b5f28300ca0e), -157 }, + { UINT64_C(0xbce5086492111aeb), -130 }, + { UINT64_C(0x8cbccc096f5088cc), -103 }, + { UINT64_C(0xd1b71758e219652c), -77 }, + { UINT64_C(0x9c40000000000000), -50 }, + { UINT64_C(0xe8d4a51000000000), -24 }, + { UINT64_C(0xad78ebc5ac620000), 3 }, + { UINT64_C(0x813f3978f8940984), 30 }, + { UINT64_C(0xc097ce7bc90715b3), 56 }, + { UINT64_C(0x8f7e32ce7bea5c70), 83 }, + { UINT64_C(0xd5d238a4abe98068), 109 }, + { UINT64_C(0x9f4f2726179a2245), 136 }, + { UINT64_C(0xed63a231d4c4fb27), 162 }, + { UINT64_C(0xb0de65388cc8ada8), 189 }, + { UINT64_C(0x83c7088e1aab65db), 216 }, + { UINT64_C(0xc45d1df942711d9a), 242 }, + { UINT64_C(0x924d692ca61be758), 269 }, + { UINT64_C(0xda01ee641a708dea), 295 }, + { UINT64_C(0xa26da3999aef774a), 322 }, + { UINT64_C(0xf209787bb47d6b85), 348 }, + { UINT64_C(0xb454e4a179dd1877), 375 }, + { UINT64_C(0x865b86925b9bc5c2), 402 }, + { UINT64_C(0xc83553c5c8965d3d), 428 }, + { UINT64_C(0x952ab45cfa97a0b3), 455 }, + { UINT64_C(0xde469fbd99a05fe3), 481 }, + { UINT64_C(0xa59bc234db398c25), 508 }, + { UINT64_C(0xf6c69a72a3989f5c), 534 }, + { UINT64_C(0xb7dcbf5354e9bece), 561 }, + { UINT64_C(0x88fcf317f22241e2), 588 }, + { UINT64_C(0xcc20ce9bd35c78a5), 614 }, + { UINT64_C(0x98165af37b2153df), 641 }, + { UINT64_C(0xe2a0b5dc971f303a), 667 }, + { UINT64_C(0xa8d9d1535ce3b396), 694 }, + { UINT64_C(0xfb9b7cd9a4a7443c), 720 }, + { UINT64_C(0xbb764c4ca7a44410), 747 }, + { UINT64_C(0x8bab8eefb6409c1a), 774 }, + { UINT64_C(0xd01fef10a657842c), 800 }, + { UINT64_C(0x9b10a4e5e9913129), 827 }, + { UINT64_C(0xe7109bfba19c0c9d), 853 }, + { UINT64_C(0xac2820d9623bf429), 880 }, + { UINT64_C(0x80444b5e7aa7cf85), 907 }, + { UINT64_C(0xbf21e44003acdd2d), 933 }, + { UINT64_C(0x8e679c2f5e44ff8f), 960 }, + { UINT64_C(0xd433179d9c8cb841), 986 }, + { UINT64_C(0x9e19db92b4e31ba9), 1013 }, + { UINT64_C(0xeb96bf6ebadf77d9), 1039 }, + { UINT64_C(0xaf87023b9bf0ee6b), 1066 }, +}; + +static DtoaDiyFp dtoa_cached_power(int e, int *dec_exp) +{ + size_t i; + + /* Select the first power which puts the scaled upper boundary in + [-60, -32]. Integer selection avoids host floating-point variation. */ + for (i = 0; i < countof(dtoa_cached_powers); i++) { + if (e + dtoa_cached_powers[i].e + 64 >= -60) { + *dec_exp = 348 - (int)i * 8; + return dtoa_cached_powers[i]; + } + } + abort(); /* Binary64's exponent range always selects an entry. */ +} + +static int dtoa_decimal_digits32(uint32_t n) +{ + if (n < 10) return 1; + if (n < 100) return 2; + if (n < 1000) return 3; + if (n < 10000) return 4; + if (n < 100000) return 5; + if (n < 1000000) return 6; + if (n < 10000000) return 7; + if (n < 100000000) return 8; + return 9; +} + +static bool dtoa_grisu_round(char *digits, int len, + uint64_t upper_distance, + uint64_t unsafe_interval, uint64_t rest, + uint64_t ten_kappa, uint64_t unit) +{ + uint64_t small_distance = upper_distance - unit; + uint64_t big_distance = upper_distance + unit; + + while (rest < small_distance && + unsafe_interval - rest >= ten_kappa && + (rest + ten_kappa < small_distance || + small_distance - rest >= rest + ten_kappa - small_distance)) { + digits[len - 1]--; + rest += ten_kappa; + } + /* If two digits are still plausible, the bignum fallback must decide. */ + if (rest < big_distance && unsafe_interval - rest >= ten_kappa && + (rest + ten_kappa < big_distance || + big_distance - rest > rest + ten_kappa - big_distance)) + return false; + return 2 * unit <= rest && rest <= unsafe_interval - 4 * unit; +} + +static bool dtoa_grisu_digits(DtoaDiyFp w, DtoaDiyFp lower, + DtoaDiyFp upper, char *digits, + int *plen, int *pdec_exp) +{ + static const uint64_t pow10[] = { + UINT64_C(1), UINT64_C(10), UINT64_C(100), UINT64_C(1000), + UINT64_C(10000), UINT64_C(100000), UINT64_C(1000000), + UINT64_C(10000000), UINT64_C(100000000), UINT64_C(1000000000), + UINT64_C(10000000000), UINT64_C(100000000000), + UINT64_C(1000000000000), UINT64_C(10000000000000), + UINT64_C(100000000000000), UINT64_C(1000000000000000), + UINT64_C(10000000000000000), UINT64_C(100000000000000000), + UINT64_C(1000000000000000000), UINT64_C(10000000000000000000) + }; + uint64_t unit = 1; + uint64_t one, upper_distance, unsafe_interval, p2; + uint32_t p1; + int kappa; + int len = 0; + + /* Cached-power multiplication is rounded by at most one unit. Widen the + interval by that error; round-weed accepts only unambiguous results. */ + lower.f -= unit; + upper.f += unit; + one = (uint64_t)1 << -upper.e; + upper_distance = upper.f - w.f; + unsafe_interval = upper.f - lower.f; + p1 = upper.f >> -upper.e; + p2 = upper.f & (one - 1); + kappa = dtoa_decimal_digits32(p1); + + while (kappa > 0) { + uint32_t divisor = (uint32_t)pow10[kappa - 1]; + uint32_t digit = p1 / divisor; + uint64_t rest; + p1 %= divisor; + if (digit || len) + digits[len++] = '0' + digit; + kappa--; + rest = ((uint64_t)p1 << -upper.e) + p2; + if (rest < unsafe_interval) { + *pdec_exp += kappa; + *plen = len; + return dtoa_grisu_round(digits, len, upper_distance, + unsafe_interval, rest, + pow10[kappa] << -upper.e, unit); + } + } + for (;;) { + uint32_t digit; + p2 *= 10; + unit *= 10; + unsafe_interval *= 10; + digit = p2 >> -upper.e; + if (digit || len) + digits[len++] = '0' + digit; + p2 &= one - 1; + kappa--; + if (p2 < unsafe_interval) { + *pdec_exp += kappa; + *plen = len; + return dtoa_grisu_round(digits, len, upper_distance * unit, + unsafe_interval, p2, one, unit); + } + } +} + +static bool dtoa_grisu3(uint64_t bits, uint64_t *pmant, int *pdigits, + int *pdec_exp) +{ + uint64_t frac = bits & (UINT64_C(1) << 52) - 1; + int biased_e = (bits >> 52) & 0x7ff; + DtoaDiyFp v, lower, upper, c, w, lower_scaled, upper_scaled; + char digits[18]; + uint64_t mant = 0; + int len, i; + + if (biased_e == 0) { + v.f = frac; + v.e = -1074; + } else { + v.f = frac | (UINT64_C(1) << 52); + v.e = biased_e - 1075; + } + + upper.f = (v.f << 1) + 1; + upper.e = v.e - 1; + upper = dtoa_diy_normalize(upper); + if (v.f == (UINT64_C(1) << 52)) { + lower.f = (v.f << 2) - 1; + lower.e = v.e - 2; + } else { + lower.f = (v.f << 1) - 1; + lower.e = v.e - 1; + } + lower.f <<= lower.e - upper.e; + lower.e = upper.e; + + c = dtoa_cached_power(upper.e, pdec_exp); + w = dtoa_diy_mul(dtoa_diy_normalize(v), c); + lower_scaled = dtoa_diy_mul(lower, c); + upper_scaled = dtoa_diy_mul(upper, c); + if (!dtoa_grisu_digits(w, lower_scaled, upper_scaled, + digits, &len, pdec_exp)) + return false; + if (len <= 0 || len > 17) + return false; + for (i = 0; i < len; i++) + mant = mant * 10 + digits[i] - '0'; + *pmant = mant; + *pdigits = len; + return true; +} + /* return the length */ int js_dtoa(char *buf, double d, int radix, int n_digits, int flags, JSDTOATempMem *tmp_mem) @@ -1175,11 +1485,23 @@ int js_dtoa(char *buf, double d, int radix, int n_digits, int flags, goto done; } #endif - + + /* Local deviation from upstream QuickJS: avoid the exact shortest-digit + search for ordinary decimal String(number)/JSON number formatting. */ + if (radix == 10 && fmt == JS_DTOA_FORMAT_FREE && + (flags & JS_DTOA_EXP_MASK) == JS_DTOA_EXP_AUTO) { + int dec_exp; + if (dtoa_grisu3(a & ~(UINT64_C(1) << 63), &m, &P, &dec_exp)) { + E = P + dec_exp; + mpb_set_u64(tmp1, m); + goto output; + } + } + /* this choice of E implies F=round(x*B^(P-E) is such as: B^(P-1) <= F < 2.B^P. */ E = 1 + mul_log2_radix(e - 1, radix); - + if (fmt == JS_DTOA_FORMAT_FREE) { int P_max, E0, e1, E_found, P_found; uint64_t m1, mant_found, mant, mant_max1; @@ -1380,7 +1702,7 @@ double js_atod(const char *str, const char **pnext, int radix, int flags, } else { p_start = p; } - + if (p[0] == '0') { if ((p[1] == 'x' || p[1] == 'X') && (radix == 0 || radix == 16)) { @@ -1456,7 +1778,7 @@ double js_atod(const char *str, const char **pnext, int radix, int flags, p++; pos++; } - + sig_pos = pos; for(;;) { limb_t c; @@ -1502,13 +1824,13 @@ double js_atod(const char *str, const char **pnext, int radix, int flags, dot_pos = pos; expn_offset = sig_pos + digit_count - dot_pos; } - + /* Use the extra digits for rounding if the base is a power of two. Otherwise they are just truncated. */ if (radix_bits != 0 && extra_digits != 0) { tmp0->tab[0] |= 1; } - + /* parse the exponent, if any */ expn = 0; expn_overflow = false; diff --git a/third_party/quickjs/quickjs-atom.h b/third_party/quickjs/quickjs-atom.h index 6d917c4..a0ae022 100644 --- a/third_party/quickjs/quickjs-atom.h +++ b/third_party/quickjs/quickjs-atom.h @@ -79,6 +79,24 @@ DEF(safe, "safe") DEF(mut, "mut") DEF(unsafe, "unsafe") DEF(extern, "extern") +/* SX scalar type names. Ordinary identifiers, not keywords: they sit + past JS_ATOM_LAST_STRICT_KEYWORD so `let i32 = 1` still parses. + + Adding or removing any atom in this file shifts every index after it, and + the checked-in bytecode blobs (builtin-array-fromasync.h, + builtin-iterator-zip.h, builtin-iterator-zip-keyed.h) encode predefined + atoms by index. Regenerate all three, or they fail at runtime somewhere + unrelated -- adding these four surfaced as "ReferenceError: get is not + defined" out of Array.fromAsync: + + qjsc -C -ss -o builtin-array-fromasync.h builtin-array-fromasync.js + + They cannot be generated by the build: quickjs.c includes them, and qjsc + is linked against quickjs.c. */ +DEF(i32, "i32") +DEF(f32, "f32") +DEF(f64, "f64") +DEF(bool, "bool") /* empty string */ DEF(empty_string, "") diff --git a/third_party/quickjs/quickjs.c b/third_party/quickjs/quickjs.c index 8a9de1a..4ace397 100644 --- a/third_party/quickjs/quickjs.c +++ b/third_party/quickjs/quickjs.c @@ -837,7 +837,7 @@ typedef struct JSClosureVar { uint8_t is_const : 1; /* const variable (is_lexical = 1 if is_const = 1) */ uint8_t var_kind : 4; /* see JSVarKindEnum */ uint8_t is_safe_i32 : 1; /* SX `safe ... : i32` binding: stores must wrap */ - /* 6 bits available */ + /* 2 bits available */ uint16_t var_idx; /* JS_CLOSURE_LOCAL/JS_CLOSURE_ARG: index to a normal variable of the parent function. otherwise: index to a closure variable of the parent function */ @@ -875,6 +875,25 @@ typedef enum { allocated immediately after it). */ } JSVarKindEnum; +/* SX declared types. What an annotation said, recorded rather than thrown + away, so codegen can specialize on the type that was actually written + instead of on the single `safe` bit. SX_TYPE_NONE means no annotation; + SX_TYPE_OTHER means one was written that codegen cannot act on (a union, + a generic, an inline object type). Four bits, so any addition past + SX_TYPE_NUMBER needs the bitfields below widened. Only the types codegen + acts on are named: a struct, a borrow, `string` and `void` are all + SX_TYPE_OTHER, because nothing reads them and a value nothing reads is a + value that goes stale. Add one back with its consumer, not before. */ +typedef enum SxType { + SX_TYPE_NONE = 0, + SX_TYPE_OTHER, + SX_TYPE_I32, + SX_TYPE_F32, + SX_TYPE_F64, + SX_TYPE_BOOL, + SX_TYPE_NUMBER, +} SxType; + /* XXX: could use a different structure in bytecode functions to save memory */ typedef struct JSVarDef { @@ -899,6 +918,14 @@ typedef struct JSVarDef { uint8_t is_sx_module_local : 1; /* module top-level `safe let` kept as a plain local; demotable back to a module var if a later `export {}` clause names it */ + uint8_t is_sx_mut : 1; /* SX `let mut`: a mutable owner, so `&mut` may + borrow it exclusively (spec/LANGUAGE.md) */ + /* SxType: the declared annotation, or SX_TYPE_NONE. Compile-time only, + like is_safe_i32, and so deliberately not written to .sxbc: everything + that reads it -- the i32 opcode gating and sx_inline_typed_calls -- + has already run by the time bytecode is serialized, and its effect is + baked into the emitted code. */ + uint8_t sx_type : 4; uint8_t var_kind : 4; /* see JSVarKindEnum */ /* if is_captured = true, provides the index of the corresponding JSVarRef on stack */ @@ -943,6 +970,9 @@ typedef struct JSFunctionBytecode { uint8_t gc_free_candidate : 1; /* no potentially allocating bytecodes */ /* 0=unknown, 1=the immutable captured-add callback shape, 2=not it. */ uint8_t fast_captured_add : 2; + /* SxType of the declared return annotation. Parameter types need no + field: vardefs[0..arg_count) are JSVarDefs and carry their own. */ + uint8_t sx_ret_type : 4; /* Closure cell used by the cached captured-add shape. */ uint16_t fast_captured_add_ref; uint8_t *byte_code_buf; /* (self pointer) */ @@ -4746,6 +4776,20 @@ static no_inline int string_buffer_putc16_slow(StringBuffer *s, uint32_t c) } /* 0 <= c <= 0xff */ +static int string_buffer_putc8(StringBuffer *s, uint32_t c); + +/* The one-byte case, inline: JSON.stringify calls this for every brace, + comma, colon and quote -- half a dozen times per property -- and the call + itself was costing more than the store. */ +static inline int string_buffer_putc8_fast(StringBuffer *s, uint32_t c) +{ + if (likely(s->len < s->size && !s->is_wide_char)) { + str8(s->str)[s->len++] = c; + return 0; + } + return string_buffer_putc8(s, c); +} + static int string_buffer_putc8(StringBuffer *s, uint32_t c) { if (unlikely(s->len >= s->size)) { @@ -4936,6 +4980,20 @@ static int string_buffer_concat_value_free(StringBuffer *s, JSValue v) return -1; } tag = JS_VALUE_GET_TAG(v); + /* An integer's digits, and the three literals, go straight into the + buffer. The general path below asks JS_ToString for them, which + allocates a string, copies it in and frees it -- once per value, and a + JSON document is mostly these. */ + if (tag == JS_TAG_INT) { + char buf[16]; + size_t len = i64toa(buf, JS_VALUE_GET_INT(v)); + return string_buffer_write8(s, (const uint8_t *)buf, (int)len); + } + if (tag == JS_TAG_BOOL) + return JS_VALUE_GET_BOOL(v) ? string_buffer_write8(s, (const uint8_t *)"true", 4) + : string_buffer_write8(s, (const uint8_t *)"false", 5); + if (tag == JS_TAG_NULL) + return string_buffer_write8(s, (const uint8_t *)"null", 4); if (tag == JS_TAG_STRING_ROPE) { /* concatenate rope (don't free since concat_value doesn't free) */ res = string_buffer_concat_value(s, v); @@ -5497,7 +5555,14 @@ const char *JS_ToCStringLen2(JSContext *ctx, size_t *plen, JSValueConst val1, strings, which is the most common case. */ count = 0; - for (pos = 0; pos < len; pos++) { + pos = 0; + /* Eight bytes at a time while they stay ASCII, which for the common + case -- an ASCII string, returned below without a copy at all -- + is the whole scan. Only a string that does contain a non-ASCII + byte pays the per-byte count, and only from where it starts. */ + while (pos + 8 <= len && !(get_u64(src + pos) & 0x8080808080808080ULL)) + pos += 8; + for (; pos < len; pos++) { count += src[pos] >> 7; } if (count == 0 && str->kind == JS_STRING_KIND_NORMAL) { @@ -5529,6 +5594,27 @@ const char *JS_ToCStringLen2(JSContext *ctx, size_t *plen, JSValueConst val1, q = str8(str_new); pos = 0; while (pos < len) { + /* Four code points at a time while they are all ASCII: text is + mostly ASCII even when one accent makes the whole string + wide, and this loop is what every JS_ToCString of such a + string spends its time in. */ +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + while (pos + 4 <= len) { + uint64_t v = get_u64((const uint8_t *)(src + pos)); + /* Little-endian only: the mask says "the high byte of each + 16-bit unit is zero and its low byte is below 0x80". */ + if (v & 0xff80ff80ff80ff80ULL) + break; + q[0] = (uint8_t)src[pos]; + q[1] = (uint8_t)src[pos + 1]; + q[2] = (uint8_t)src[pos + 2]; + q[3] = (uint8_t)src[pos + 3]; + q += 4; + pos += 4; + } +#endif + if (pos >= len) + break; c = src[pos++]; if (c < 0x80) { *q++ = c; @@ -15964,26 +16050,91 @@ static JSValue JS_ToStringCheckObject(JSContext *ctx, JSValueConst val) return JS_ToString(ctx, val); } -static JSValue JS_ToQuotedString(JSContext *ctx, JSValueConst val1) +/* Characters a JSON string can carry unescaped: not a quote, not a + backslash, not a control character. Eight bytes at a time -- most strings + need no escaping at all, and the per-character loop this replaces was the + largest single cost in JSON.stringify. */ +static int json_quote_run8(const uint8_t *p, int len) +{ + int i = 0; +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + while (i + 8 <= len) { + uint64_t v = get_u64(p + i); + uint64_t quote = v ^ 0x2222222222222222ULL; + uint64_t slash = v ^ 0x5c5c5c5c5c5c5c5cULL; + /* (x - 0x01..) & ~x sets the high bit of any byte that was zero, + which is how each of these three tests finds its byte. */ + uint64_t mask = (((quote - 0x0101010101010101ULL) & ~quote) + | ((slash - 0x0101010101010101ULL) & ~slash) + | ((v - 0x2020202020202020ULL) & ~v)) + & 0x8080808080808080ULL; + if (mask) + return i + (ctz64(mask) >> 3); + i += 8; + } +#endif + while (i < len) { + uint8_t c = p[i]; + if (c == '"' || c == '\\' || c < 0x20) + break; + i++; + } + return i; +} + +/* The same for a wide string. A surrogate ends the run, so the character + loop below still pairs it up or escapes it as it always did. */ +static int json_quote_run16(const uint16_t *p, int len) +{ + int i = 0; + while (i < len) { + uint16_t c = p[i]; + if (c == '"' || c == '\\' || c < 0x20 || is_surrogate(c)) + break; + i++; + } + return i; +} + +/* Writes the quoted form of `val1` into a buffer the caller already has. + JSON.stringify used to allocate a quoted string for every key and every + string value, copy it into its output buffer, and free it again. */ +static int string_buffer_quote(StringBuffer *b, JSValueConst val1) { + JSContext *ctx = b->ctx; JSValue val; JSString *p; - int i; + int i, run; uint32_t c; - StringBuffer b_s, *b = &b_s; char buf[16]; val = JS_ToStringCheckObject(ctx, val1); if (JS_IsException(val)) - return val; + return -1; p = JS_VALUE_GET_STRING(val); - if (string_buffer_init(ctx, b, p->len + 2)) - goto fail; - - if (string_buffer_putc8(b, '\"')) + if (string_buffer_putc8_fast(b, '\"')) goto fail; for(i = 0; i < p->len; ) { + /* Everything up to the next character needing an escape goes over in + one copy; in the common case that is the whole string. */ + if (p->is_wide_char) { + run = json_quote_run16(str16(p) + i, p->len - i); + if (run > 0) { + if (string_buffer_write16(b, str16(p) + i, run)) + goto fail; + i += run; + continue; + } + } else { + run = json_quote_run8(str8(p) + i, p->len - i); + if (run > 0) { + if (string_buffer_write8(b, str8(p) + i, run)) + goto fail; + i += run; + continue; + } + } c = string_getc(p, &i); switch(c) { case '\t': @@ -16004,9 +16155,9 @@ static JSValue JS_ToQuotedString(JSContext *ctx, JSValueConst val1) case '\"': case '\\': quote: - if (string_buffer_putc8(b, '\\')) + if (string_buffer_putc8_fast(b, '\\')) goto fail; - if (string_buffer_putc8(b, c)) + if (string_buffer_putc8_fast(b, c)) goto fail; break; default: @@ -16021,14 +16172,26 @@ static JSValue JS_ToQuotedString(JSContext *ctx, JSValueConst val1) break; } } - if (string_buffer_putc8(b, '\"')) + if (string_buffer_putc8_fast(b, '\"')) goto fail; JS_FreeValue(ctx, val); - return string_buffer_end(b); + return 0; fail: JS_FreeValue(ctx, val); - string_buffer_free(b); - return JS_EXCEPTION; + return -1; +} + +static JSValue JS_ToQuotedString(JSContext *ctx, JSValueConst val1) +{ + StringBuffer b_s, *b = &b_s; + + if (string_buffer_init(ctx, b, 32)) + return JS_EXCEPTION; + if (string_buffer_quote(b, val1)) { + string_buffer_free(b); + return JS_EXCEPTION; + } + return string_buffer_end(b); } static __maybe_unused void JS_DumpObjectHeader(JSRuntime *rt) @@ -19478,8 +19641,20 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, alloca_size = sizeof(JSValue) * (arg_allocated_size + b->var_count + b->stack_size) + sizeof(JSVarRef *) * b->var_ref_count; + /* arcsx: -DSXN_ABLATE_CALL_PROLOGUE removes the two pieces of prologue + work a statically known callee could be proven not to need -- the + stack-overflow check and the GC-free section -- so an A/B measures the + exact upper bound of specializing this path on a declared signature. + The rest of the prologue is not ablatable: the var_buf fill, the + var_refs fill and the realm switch change meaning rather than merely + repeating known work, and a build without them crashes during + bootstrap. Their cost is in any case zero for the shape that matters, + a small function with no locals and no closure. Behaviourally + identical on code that does not exhaust the stack. */ +#ifndef SXN_ABLATE_CALL_PROLOGUE if (js_check_stack_overflow(rt, alloca_size)) return JS_ThrowStackOverflow(caller_ctx); +#endif sf->is_strict_mode = b->is_strict_mode; arg_buf = (JSValue *)argv; @@ -19513,10 +19688,12 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, sf->cur_gc_obj = NULL; sp = stack_buf; pc = b->byte_code_buf; +#ifndef SXN_ABLATE_CALL_PROLOGUE if (b->gc_free_candidate && b->func_kind == JS_FUNC_NORMAL) { JS_EnterGCFreeSection(rt); gc_free_entered = true; } +#endif /* sf->cur_pc must we set to pc before any recursive calls to JS_CallInternal. */ sf->cur_pc = NULL; sf->prev_frame = rt->current_stack_frame; @@ -23941,6 +24118,9 @@ typedef struct JSGlobalVar { uint8_t is_lexical : 1; /* global let/const definition */ uint8_t is_const : 1; /* const definition */ uint8_t is_safe_i32 : 1; /* SX `safe ... : i32` binding: stores must wrap */ + uint8_t is_sx_mut : 1; /* SX `let mut`, for a module/script top-level + binding, which lives here rather than in + fd->vars. Same meaning as JSVarDef.is_sx_mut. */ int scope_level; /* scope of definition */ JSAtom var_name; /* variable name */ } JSGlobalVar; @@ -24032,6 +24212,7 @@ typedef struct JSFunctionDef { bool safe_next_decl : 1; /* contextual `safe let/const` prefix */ bool sx_safe_module_local : 1; /* the define_var() call in progress must keep the binding a plain local, not a module var */ + uint8_t sx_ret_type : 4; /* SxType of this function's return annotation */ JSFunctionKindEnum func_kind : 8; JSParseFunctionEnum func_type : 7; @@ -24132,6 +24313,12 @@ typedef struct JSToken { struct { JSValue str; int sep; + /* JSON only: an escape-free ASCII string is left as a slice of + the source here, and turned into a string -- or straight into + an atom, for a property name -- only once its use is known. + NULL when `str` already holds the value. */ + const uint8_t *raw; + int raw_len; } str; struct { JSValue val; @@ -24164,6 +24351,16 @@ typedef struct JSParseState { const uint8_t *eol; // most recently seen end-of-line character const uint8_t *mark; // first token character, invariant: eol < mark + /* JSON only: the last few property names seen, so a key that repeats + from one object to the next -- which is every key in a document of + records -- is not hashed into the atom table again. Each entry holds + one reference; json_free_key_cache drops them. */ + struct { + const uint8_t *ptr; + uint32_t len; + JSAtom atom; + } json_keys[4]; + /* current function code */ JSFunctionDef *cur_func; bool is_module; /* parsing a module */ @@ -25345,6 +25542,31 @@ static int json_parse_error(JSParseState *s, const uint8_t *curp, const char *ms msg, position, line, (int)(p - line_start) + 1); } +/* The first byte a JSON string cannot pass through untouched: a closing + quote, a backslash, a control character, or anything non-ASCII. Eight + bytes at a time -- byte-at-a-time scanning was the largest single cost in + JSON.parse. */ +static const uint8_t *json_scan_plain(const uint8_t *p, const uint8_t *end) +{ +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + while (p + 8 <= end) { + uint64_t v = get_u64(p); + uint64_t quote = v ^ 0x2222222222222222ULL; + uint64_t slash = v ^ 0x5c5c5c5c5c5c5c5cULL; + uint64_t mask = (((quote - 0x0101010101010101ULL) & ~quote) + | ((slash - 0x0101010101010101ULL) & ~slash) + | ((v - 0x2020202020202020ULL) & ~v) + | v) & 0x8080808080808080ULL; + if (mask) + return p + (ctz64(mask) >> 3); + p += 8; + } +#endif + while (p < end && *p != '"' && *p != '\\' && *p >= 0x20 && *p < 0x80) + p++; + return p; +} + static int json_parse_string(JSParseState *s, const uint8_t **pp) { const uint8_t *p, *p_next; @@ -25356,6 +25578,26 @@ static int json_parse_string(JSParseState *s, const uint8_t **pp) goto fail; p = *pp; + + /* A string with no escape and nothing above ASCII -- most strings in most + JSON -- is handed on as a slice of the source, with no string built at + all: a property name becomes an atom directly, and only a value is + allocated. The StringBuffer below starts at 48 characters and is then + grown, copied and trimmed, which is what this skips. */ + { + const uint8_t *q = json_scan_plain(p, s->buf_end); + if (q < s->buf_end && *q == '"') { + string_buffer_free(b); + s->token.val = TOK_STRING; + s->token.u.str.sep = '"'; + s->token.u.str.str = JS_UNDEFINED; + s->token.u.str.raw = p; + s->token.u.str.raw_len = (int)(q - p); + *pp = q + 1; + return 0; + } + } + for(;;) { if (p >= s->buf_end) { goto end_of_input; @@ -25363,9 +25605,7 @@ static int json_parse_string(JSParseState *s, const uint8_t **pp) // Fast path: batch consecutive ASCII characters const uint8_t *p_start = p; - while (p < s->buf_end && *p != '"' && *p != '\\' && *p >= 0x20 && *p < 0x80) { - p++; - } + p = json_scan_plain(p, s->buf_end); // Write batched ASCII in one call if (p > p_start) { @@ -25426,6 +25666,7 @@ static int json_parse_string(JSParseState *s, const uint8_t **pp) s->token.val = TOK_STRING; s->token.u.str.sep = '"'; s->token.u.str.str = string_buffer_end(b); + s->token.u.str.raw = NULL; *pp = p; return 0; @@ -25453,6 +25694,7 @@ static int json_parse_number(JSParseState *s, const uint8_t **pp) while (is_digit(*p)) p++; + const uint8_t *p_int_end = p; if (*p == '.') { p++; if (!is_digit(*p)) @@ -25470,7 +25712,26 @@ static int json_parse_number(JSParseState *s, const uint8_t **pp) p++; } s->token.val = TOK_NUMBER; - s->token.u.num.val = js_float64(strtod((const char *)p_start, NULL)); + /* A plain integer is converted here: strtod is locale-aware -- it takes a + lock to find the decimal separator -- and rescans the digits. 18 digits + is the widest that always fits in an int64. */ + if (p == p_int_end && (size_t)(p_int_end - p_start) <= 18) { + const uint8_t *d = p_start; + int negative = (*d == '-' || *d == '+') ? (*d++ == '-') : 0; + int64_t v = 0; + while (d < p_int_end) + v = v * 10 + (*d++ - '0'); + if (negative) + v = -v; + if (negative && v == 0) + s->token.u.num.val = js_float64(-0.0); /* -0 is not 0 */ + else if (v >= INT32_MIN && v <= INT32_MAX) + s->token.u.num.val = js_int32((int32_t)v); + else + s->token.u.num.val = js_float64((double)v); + } else { + s->token.u.num.val = js_float64(strtod((const char *)p_start, NULL)); + } *pp = p; return 0; } @@ -26408,6 +26669,7 @@ static JSGlobalVar *add_global_var(JSContext *ctx, JSFunctionDef *s, hf->is_lexical = false; hf->is_const = false; hf->is_safe_i32 = false; + hf->is_sx_mut = false; hf->scope_level = s->scope_level; hf->var_name = JS_DupAtom(ctx, name); if (update_global_var_htab(ctx, s)) @@ -26598,7 +26860,8 @@ static __exception int js_parse_function_decl2(JSParseState *s, static __exception int js_parse_assign_expr2(JSParseState *s, int parse_flags); static __exception int js_parse_assign_expr(JSParseState *s); static __exception int js_parse_unary(JSParseState *s, int parse_flags); -static __exception int js_parse_type_annotation(JSParseState *s, bool return_type); +static __exception int js_parse_type_annotation(JSParseState *s, bool return_type, + SxType *out); static void push_break_entry(JSFunctionDef *fd, BlockEnv *be, JSAtom label_name, int label_break, int label_cont, @@ -27054,7 +27317,9 @@ static int js_parse_skip_parens_token(JSParseState *s, int *pbits, bool no_line_ ternary's `(a, b) : c`, etc.) is ordinary JS and must not be swallowed. */ if (tok == ':' && skip_return_type) { - if (js_parse_type_annotation(s, true)) + /* This is lookahead only -- js_parse_seek_token rewinds + below and the real parse records the type. */ + if (js_parse_type_annotation(s, true, NULL)) tok = TOK_EOF; else tok = s->token.val; @@ -29712,6 +29977,38 @@ static __exception int js_parse_unary(JSParseState *s, int parse_flags) if (s->token.val == TOK_IDENT && s->token.u.ident.atom == JS_ATOM_mut) { if (next_token(s)) return -1; + /* `&mut value` is an exclusive borrow and requires a mutable + owner (spec/LANGUAGE.md). Only a bare identifier can be + resolved here, and only against this function's lexical + scope chain: a miss is a parameter, a global, a captured + outer binding or a name not yet declared, none of which this + parser can rule on, so it stays silent rather than guessing. + `&mut this.x` and `&mut f()` never reach the lookup. */ + if (s->token.val == TOK_IDENT) { + JSAtom name = s->token.u.ident.atom; + JSFunctionDef *fd = s->cur_func; + int idx = find_lexical_decl(s->ctx, fd, name, + fd->scope_first, false); + bool found = false, is_mut = false; + if (idx >= 0 && idx < GLOBAL_VAR_OFFSET) { + found = true; + is_mut = fd->vars[idx].is_sx_mut; + } else { + /* Module and script top-level lexicals live in + fd->global_vars instead (see js_define_var). */ + JSGlobalVar *hf = find_lexical_global_var(fd, name); + if (hf) { + found = true; + is_mut = hf->is_sx_mut; + } + } + if (found && !is_mut) { + char buf[ATOM_GET_STR_BUF_SIZE]; + return js_parse_error(s, + "SX2003: cannot borrow immutable binding '%s' as '&mut'; declare it 'let mut'", + JS_AtomGetStr(s->ctx, buf, sizeof(buf), name)); + } + } } return js_parse_unary(s, parse_flags); case '+': @@ -29835,16 +30132,53 @@ static __exception int js_parse_unary(JSParseState *s, int parse_flags) return 0; } -/* Skip an erasable SX/TypeScript type after the colon. The parser keeps the - declaration and its binding metadata while discarding only type tokens. */ -static __exception int js_parse_type_annotation(JSParseState *s, bool return_type) +/* An SX `safe` binding earns the wrapping i32 opcodes only when i32 is what + was declared. `safe` alone used to select them, which is why + `safe let mut x: f64` reached an integer opcode at all and had to be + rescued by its runtime tag test. Note this is deliberately *not* the same + condition as is_i32_inferred, which is a plain-JS induction guess and + carries no promise about staying in range. */ +static inline bool sx_is_safe_i32(const JSVarDef *vd) +{ + return vd->is_safe && vd->sx_type == SX_TYPE_I32; +} + +/* Name the current token if it is a scalar type codegen can act on. */ +static SxType sx_classify_type_token(JSParseState *s) +{ + if (s->token.val != TOK_IDENT) return SX_TYPE_OTHER; + switch (s->token.u.ident.atom) { + case JS_ATOM_i32: return SX_TYPE_I32; + case JS_ATOM_f32: return SX_TYPE_F32; + case JS_ATOM_f64: return SX_TYPE_F64; + case JS_ATOM_bool: + case JS_ATOM_boolean: return SX_TYPE_BOOL; + case JS_ATOM_number: return SX_TYPE_NUMBER; + default: return SX_TYPE_OTHER; + } +} + +/* Skip an erasable SX/TypeScript type after the colon, classifying it on the + way past. The declaration and its binding metadata are kept; only the type + tokens are discarded from the emitted code. + + `out` receives the SxType, which codegen may specialize on, so it has to be + exact rather than optimistic: only an annotation that is a single scalar + type token is named. A union, a generic, an array, an inline object type + and a borrow are all more than one token, and report SX_TYPE_OTHER. The + skipping itself is unchanged: it is what lets arbitrary TypeScript still + parse, and narrowing it would reject real programs. */ +static __exception int js_parse_type_annotation(JSParseState *s, bool return_type, + SxType *out) { int depth = 0; - if (s->token.val != ':') return 0; + int content = 0; + SxType first = SX_TYPE_NONE; + if (s->token.val != ':') { if (out) *out = SX_TYPE_NONE; return 0; } for (;;) { if (next_token(s)) return -1; if (!depth && return_type && (s->token.val == '{' || s->token.val == TOK_ARROW)) - return 0; + break; if (s->token.val == '<' || s->token.val == '[' || s->token.val == '{' || s->token.val == '(') depth++; else if ((s->token.val == '>' || s->token.val == ']' || s->token.val == '}' || s->token.val == ')') && depth) @@ -29856,8 +30190,16 @@ static __exception int js_parse_type_annotation(JSParseState *s, bool return_typ if (!depth && (s->token.val == '=' || s->token.val == ',' || s->token.val == ';' || s->token.val == ')' || (return_type && (s->token.val == TOK_ARROW)))) - return 0; - } + break; + if (content == 0) + first = sx_classify_type_token(s); + content++; + } + if (out) + *out = content == 0 ? SX_TYPE_NONE + : content == 1 ? first + : SX_TYPE_OTHER; + return 0; } /* arcsx: skip a type expression that is not introduced by ':' -- the operand @@ -30750,6 +31092,32 @@ static __exception int js_parse_block(JSParseState *s) return 0; } +/* Attach SX metadata to a binding that was just declared. A function-scoped + lexical is a JSVarDef in fd->vars; a module or script top-level one is a + JSGlobalVar instead -- the same split fd->sx_safe_module_local exists for. + Only `is_sx_mut` needs to reach the JSGlobalVar, because that is the one + thing `&mut` asks about at top level; `is_safe` and the declared type are + read only off fd->vars, and a `safe` module-level binding is kept there by + fd->sx_safe_module_local. SX_TYPE_NONE leaves the recorded type alone, so + a later call cannot erase an earlier one. */ +static void sx_mark_decl(JSFunctionDef *fd, JSAtom name, + bool set_safe, bool set_mut, SxType type) +{ + JSGlobalVar *hf; + int idx; + for (idx = fd->var_count - 1; idx >= 0; --idx) { + if (fd->vars[idx].var_name == name) { + if (set_safe) fd->vars[idx].is_safe = 1; + if (set_mut) fd->vars[idx].is_sx_mut = 1; + if (type != SX_TYPE_NONE) fd->vars[idx].sx_type = type; + return; + } + } + hf = find_lexical_global_var(fd, name); + if (!hf) return; + if (set_mut) hf->is_sx_mut = 1; +} + /* allowed parse_flags: PF_IN_ACCEPTED */ static __exception int js_parse_var(JSParseState *s, int parse_flags, int tok, bool export_flag) @@ -30761,10 +31129,15 @@ static __exception int js_parse_var(JSParseState *s, int parse_flags, int tok, for (;;) { /* `let mut` erases the mutability qualifier unconditionally -- not just under `safe`, matching the compatibility frontend's - `let mut` -> `let ` rewrite. */ + `let mut` -> `let ` rewrite. The qualifier is erased from the + emitted code but remembered on the JSVarDef, because `&mut x` + requires a mutable owner (spec/LANGUAGE.md). Declared per + declarator, so `let mut a = 1, b = 2` marks only `a`. */ + bool is_mut = false; if (tok == TOK_LET && s->token.val == TOK_IDENT && s->token.u.ident.atom == JS_ATOM_mut) { if (next_token(s)) goto var_error; + is_mut = true; } if (s->token.val == TOK_IDENT) { if (s->token.u.ident.is_reserved) { @@ -30792,14 +31165,8 @@ static __exception int js_parse_var(JSParseState *s, int parse_flags, int tok, goto var_error; } fd->sx_safe_module_local = 0; - if (fd->safe_next_decl) { - int safe_idx; - for (safe_idx = fd->var_count - 1; safe_idx >= 0; --safe_idx) { - if (fd->vars[safe_idx].var_name == name) { - fd->vars[safe_idx].is_safe = 1; - break; - } - } + if (fd->safe_next_decl || is_mut) { + sx_mark_decl(fd, name, fd->safe_next_decl, is_mut, SX_TYPE_NONE); fd->safe_next_decl = 0; } /* peek_token() is the lightweight simple_next_token() lookahead, @@ -30819,11 +31186,14 @@ static __exception int js_parse_var(JSParseState *s, int parse_flags, int tok, } } } - /* Minimal native SX annotation support for bindings. Type names - are compile-time metadata; skip the token sequence while - retaining the declaration for QuickJS bytecode generation. */ + /* Native SX annotation support for bindings. Type names emit no + code, so the token sequence is skipped -- but which type was + written is recorded on the binding, because codegen specializes + on it (see the SX_TYPE_I32 sites in optimize_bytecode). */ if (s->token.val == ':') { - if (js_parse_type_annotation(s, false)) goto var_error; + SxType declared; + if (js_parse_type_annotation(s, false, &declared)) goto var_error; + sx_mark_decl(fd, name, false, false, declared); } if (tok == TOK_USING) { /* Allocate a paired hidden local for the cached dispose @@ -38189,7 +38559,7 @@ static bool i32al_try_match(JSFunctionDef *fd, const uint8_t *buf, int bc_len, if (!i32al_match_get_loc(buf, bc_len, &pos, &i_idx)) return false; - if (!(fd->vars[i_idx].is_safe || fd->vars[i_idx].is_i32_inferred) || + if (!(sx_is_safe_i32(&fd->vars[i_idx]) || fd->vars[i_idx].is_i32_inferred) || fd->vars[i_idx].is_captured) return false; @@ -38234,7 +38604,7 @@ static bool i32al_try_match(JSFunctionDef *fd, const uint8_t *buf, int bc_len, return false; /* a captured local is observable mid-loop through its JSVarRef, so the fused native loop (which only writes var_buf back at the end) is unsound */ - if (!fd->vars[accum_idx].is_safe || fd->vars[accum_idx].is_captured) + if (!sx_is_safe_i32(&fd->vars[accum_idx]) || fd->vars[accum_idx].is_captured) return false; { @@ -38370,6 +38740,311 @@ static __exception int fuse_i32_accum_loops(JSContext *ctx, JSFunctionDef *fd) return 0; } +/* --- SX typed-call inlining -------------------------------------------- + + An interpreted call costs about 10 ns of frame setup on an M4 before a + single argument is passed, measured against an empty loop. Hand-inlining a + two-argument `i32` add recovered 11.8 of the 17.1 ns the call took, so the + frame is the cost and the arguments are not. This pass takes that back for + the one shape where doing so is provably free of consequence. + + It runs where fuse_i32_accum_loops runs -- after resolve_variables, before + resolve_labels -- so locals are already get_loc/put_loc and jumps still + carry label ids rather than byte offsets. Bytes can therefore be removed + without repairing a single jump target. + + A call site qualifies only when all of this holds statically: + + - The callee is a local written exactly once, by an `fclosure` naming a + constant-pool function, and never captured. The binding cannot be + anything else at runtime, so the splice needs no guard -- unlike the + Buffer.from and emit fusions, whose callees are ordinary dynamic values. + - Every parameter and the return carry a declared scalar SX type. That is + what makes this opt-in through the type system rather than a general + JavaScript optimization: plain .js and .mjs code never qualifies. + - The callee body loads each parameter exactly once, in declaration + order, before anything else, then runs only operand-free arithmetic and + constants before returning. The arguments the caller has already pushed + then sit on the stack in exactly the order the body wants, so the + splice is: drop the callee load, drop the call, append the body's tail. + Nothing is reordered and no temporary is needed. Argument evaluation is + untouched, including any exception it throws. + - The argument expressions are themselves branch-free loads, constants + and arithmetic, so the stack depth between the callee load and its call + is trackable without a control-flow analysis. + + Two mechanical hazards worth naming, because both are silent if missed. + The callee's bytecode is final-pass and uses short opcodes, whose byte + values overlap the temporary opcodes that are still legal in the caller at + this point (OP_TEMP_START == OP_nop + 1); every spliced byte is therefore + required to be below OP_TEMP_START, and the small constant pushes are + rewritten to OP_push_i32 on the way in. And the callee is read through + short_opcode_info() while the caller is read through opcode_info[]. + + One observable difference remains, the same one every inlining compiler + has: the callee no longer appears in a stack trace if the spliced + arithmetic throws -- `a + b` on a Symbol, say. */ + +static bool sx_type_is_scalar(int t) +{ + return t == SX_TYPE_I32 || t == SX_TYPE_F32 || t == SX_TYPE_F64 || + t == SX_TYPE_BOOL || t == SX_TYPE_NUMBER; +} + +/* Operand-free arithmetic: what a spliceable callee tail and an inlinable + argument expression may both contain. Nothing here branches, allocates, + reads a local, or touches an atom, the constant pool, `this` or + `arguments`. */ +static bool sx_inline_pure_op(int op) +{ + switch (op) { + case OP_add: case OP_sub: case OP_mul: case OP_div: case OP_mod: + case OP_pow: + case OP_and: case OP_or: case OP_xor: + case OP_shl: case OP_shr: case OP_sar: + case OP_lt: case OP_lte: case OP_gt: case OP_gte: + case OP_eq: case OP_neq: case OP_strict_eq: case OP_strict_neq: + case OP_neg: case OP_plus: case OP_not: case OP_lnot: + return true; + default: + return false; + } +} + +/* Rewrite one final-pass constant push into its long form, so the result can + be spliced into pre-final bytecode without colliding with a temporary + opcode. Returns the value in *val, or false if this is not one. */ +static bool sx_inline_const_push(const uint8_t *bc, int pos, int32_t *val) +{ + int op = bc[pos]; + if (op >= OP_push_0 && op <= OP_push_7) { *val = op - OP_push_0; return true; } + if (op == OP_push_i8) { *val = (int8_t)bc[pos + 1]; return true; } + if (op == OP_push_i16) { *val = (int16_t)get_u16(bc + pos + 1); return true; } + if (op == OP_push_i32) { *val = (int32_t)get_u32(bc + pos + 1); return true; } + return false; +} + +/* Append the callee's post-argument tail to `out`, translating what has to be + translated and refusing anything that cannot be spliced. Also used as the + eligibility test, with out == NULL for a dry run. */ +static bool sx_inline_emit_tail(const JSFunctionBytecode *b, int tail_start, + DynBuf *out) +{ + const uint8_t *bc = b->byte_code_buf; + int len = b->byte_code_len; + int pos = tail_start; + bool any = false; + while (pos < len && bc[pos] != OP_return) { + int32_t val; + int op = bc[pos]; + if (sx_inline_const_push(bc, pos, &val)) { + if (out) { + dbuf_putc(out, OP_push_i32); + dbuf_put_u32(out, (uint32_t)val); + } + pos += short_opcode_info(op).size; + } else if (sx_inline_pure_op(op) && op < OP_TEMP_START) { + if (out) + dbuf_putc(out, op); + pos += short_opcode_info(op).size; + } else { + return false; + } + any = true; + } + /* The return must be the last byte: a tail that falls through, or has + anything after it, is not a single expression. */ + return any && pos + 1 == len; +} + +/* Does `b` load every parameter once, in order, before doing anything else? + On success *tail_start is the first byte after those loads. */ +static bool sx_inline_candidate(const JSFunctionBytecode *b, int argc, + int *tail_start) +{ + const uint8_t *bc; + int len, pos, i; + + if (!b || !b->vardefs || !b->byte_code_buf) + return false; + if (b->arg_count != argc || argc < 1 || argc > 4) + return false; + if (b->var_count != 0 || b->closure_var_count != 0 || + b->var_ref_count != 0 || b->cpool_count != 0) + return false; + if (b->func_kind != JS_FUNC_NORMAL || !b->has_simple_parameter_list) + return false; + if (b->defined_arg_count != argc) + return false; + if (!sx_type_is_scalar(b->sx_ret_type)) + return false; + for (i = 0; i < argc; i++) + if (!sx_type_is_scalar(b->vardefs[i].sx_type)) + return false; + + bc = b->byte_code_buf; + len = b->byte_code_len; + pos = 0; + for (i = 0; i < argc; i++) { + int op; + if (pos >= len) + return false; + op = bc[pos]; + if (op >= OP_get_arg0 && op <= OP_get_arg3) { + if (op - OP_get_arg0 != i) + return false; + pos += 1; + } else if (op == OP_get_arg) { + if (pos + 3 > len || get_u16(bc + pos + 1) != (uint32_t)i) + return false; + pos += 3; + } else { + return false; + } + } + *tail_start = pos; + return sx_inline_emit_tail(b, pos, NULL); +} + +/* An argument expression this pass can scan past while tracking stack depth. + Deliberately branch-free: a label or a jump between the callee load and its + call abandons the match rather than being reasoned about. */ +static bool sx_inline_arg_op(int op) +{ + switch (op) { + case OP_get_loc: case OP_get_loc_check: + case OP_get_arg: case OP_get_var_ref: case OP_get_var_ref_check: + case OP_push_i32: case OP_push_const: case OP_push_atom_value: + case OP_push_true: case OP_push_false: case OP_null: case OP_undefined: + case OP_source_loc: + return true; + default: + return sx_inline_pure_op(op); + } +} + +static __exception int sx_inline_typed_calls(JSContext *ctx, JSFunctionDef *fd) +{ + DynBuf bc_out; + uint8_t *bc_buf = fd->byte_code.buf; + int bc_len = fd->byte_code.size; + int pos, pos_next, i; + bool changed = false; + int *func_of_loc = NULL; + int pending_out = -1, pending_cpool = -1, pending_depth = 0; + + if (fd->var_count <= 0 || fd->cpool_count <= 0) + return 0; + /* A direct eval can assign to any local by name, so nothing in this + function is provably written once. */ + if (fd->has_eval_call) + return 0; + + /* Which locals hold a constant-pool function and nothing else, ever. */ + func_of_loc = js_malloc(ctx, sizeof(*func_of_loc) * fd->var_count); + if (!func_of_loc) + return -1; + for (i = 0; i < fd->var_count; i++) + func_of_loc[i] = fd->vars[i].is_captured ? -2 : -1; + for (pos = 0; pos < bc_len; pos = pos_next) { + int op = bc_buf[pos]; + int idx; + pos_next = pos + opcode_info[op].size; + if (op == OP_fclosure && pos_next < bc_len && + bc_buf[pos_next] == OP_put_loc) { + int cpool_idx = (int)get_u32(bc_buf + pos + 1); + idx = (int)get_u16(bc_buf + pos_next + 1); + if (idx < fd->var_count && func_of_loc[idx] == -1 && + cpool_idx >= 0 && cpool_idx < fd->cpool_count) { + func_of_loc[idx] = cpool_idx; + pos_next += opcode_info[OP_put_loc].size; + } else if (idx < fd->var_count) { + func_of_loc[idx] = -2; + pos_next += opcode_info[OP_put_loc].size; + } + continue; + } + /* Any other mention of a local as anything but a plain read makes it + unusable: a second store, a TDZ init, a closure capture. */ + if (opcode_info[op].fmt == OP_FMT_loc && + op != OP_get_loc && op != OP_get_loc_check) { + idx = (int)get_u16(bc_buf + pos + 1); + if (idx < fd->var_count) + func_of_loc[idx] = -2; + } + /* make_loc_ref hands a local out as a writable reference and is the + one local-touching opcode whose format is not OP_FMT_loc: its + index sits after the atom. */ + if (op == OP_make_loc_ref) { + idx = (int)get_u16(bc_buf + pos + 5); + if (idx < fd->var_count) + func_of_loc[idx] = -2; + } + } + + js_dbuf_init(ctx, &bc_out); + for (pos = 0; pos < bc_len; pos = pos_next) { + int op = bc_buf[pos]; + int len = opcode_info[op].size; + pos_next = pos + len; + + if (pending_out >= 0 && op == OP_call) { + int argc = (int)get_u16(bc_buf + pos + 1); + JSValue cv = fd->cpool[pending_cpool]; + JSFunctionBytecode *cb = + JS_VALUE_GET_TAG(cv) == JS_TAG_FUNCTION_BYTECODE + ? JS_VALUE_GET_PTR(cv) : NULL; + int tail_start; + if (pending_depth == argc && + sx_inline_candidate(cb, argc, &tail_start)) { + /* Erase the callee load; the arguments already emitted after + it shift down and stay in their original order. */ + int drop = opcode_info[OP_get_loc].size; + memmove(bc_out.buf + pending_out, bc_out.buf + pending_out + drop, + bc_out.size - pending_out - drop); + bc_out.size -= drop; + sx_inline_emit_tail(cb, tail_start, &bc_out); + pending_out = -1; + changed = true; + continue; /* the OP_call is not emitted */ + } + pending_out = -1; + } + + if (pending_out >= 0) { + if (!sx_inline_arg_op(op)) { + pending_out = -1; + } else { + pending_depth += opcode_info[op].n_push - opcode_info[op].n_pop; + if (pending_depth < 0) + pending_out = -1; + } + } + + if (pending_out < 0 && op == OP_get_loc) { + int idx = (int)get_u16(bc_buf + pos + 1); + if (idx < fd->var_count && func_of_loc[idx] >= 0) { + pending_out = bc_out.size; + pending_cpool = func_of_loc[idx]; + pending_depth = 0; + } + } + dbuf_put(&bc_out, bc_buf + pos, len); + } + js_free(ctx, func_of_loc); + if (dbuf_error(&bc_out)) { + dbuf_free(&bc_out); + return -1; + } + if (!changed) { + dbuf_free(&bc_out); + return 0; + } + dbuf_free(&fd->byte_code); + fd->byte_code = bc_out; + return 0; +} + /* peephole optimizations and resolve goto/labels */ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) { @@ -39082,21 +39757,23 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) if (cc.col_num >= 0) col_num = cc.col_num; add_pc2line_info(s, bc_out.size, line_num, col_num); put_short_code(&bc_out, OP_get_loc_check, cc.idx); - dbuf_putc(&bc_out, s->vars[idx].is_safe ? OP_add_loc_safe_i32 : OP_add_loc); + dbuf_putc(&bc_out, sx_is_safe_i32(&s->vars[idx]) ? OP_add_loc_safe_i32 : OP_add_loc); dbuf_putc(&bc_out, idx); pos_next = cc.pos; break; } - /* Same for a constant right-hand side. */ + /* Same for a constant right-hand side. This used to exclude + `safe` locals outright, so `safe let mut n: i32` wrapped on + `n += step` and promoted on `n += 1` -- one declaration, + two answers. Both shapes now pick the opcode the same way. */ if (op == OP_get_loc_check && idx < s->var_count && idx < 256 && - !s->vars[idx].is_safe && code_match(&cc, pos_next, OP_push_i32, OP_add, OP_dup, OP_put_loc_check, idx, OP_drop, -1)) { if (cc.line_num >= 0) line_num = cc.line_num; if (cc.col_num >= 0) col_num = cc.col_num; add_pc2line_info(s, bc_out.size, line_num, col_num); push_short_int(&bc_out, cc.label); - dbuf_putc(&bc_out, OP_add_loc); + dbuf_putc(&bc_out, sx_is_safe_i32(&s->vars[idx]) ? OP_add_loc_safe_i32 : OP_add_loc); dbuf_putc(&bc_out, idx); pos_next = cc.pos; break; @@ -39105,7 +39782,7 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) goto no_change; /* arcsx: the same fusion for a TDZ-checked local; OP_inc_loc / OP_dec_loc carry their own uninitialized check. */ - if (op == OP_get_loc_check && idx < s->var_count && !s->vars[idx].is_safe && + if (op == OP_get_loc_check && idx < s->var_count && !sx_is_safe_i32(&s->vars[idx]) && (code_match(&cc, pos_next, M2(OP_post_dec, OP_post_inc), OP_put_loc_check, idx, OP_drop, -1) || code_match(&cc, pos_next, M2(OP_dec, OP_inc), OP_dup, OP_put_loc_check, idx, OP_drop, -1))) { if (cc.line_num >= 0) line_num = cc.line_num; @@ -39123,7 +39800,7 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) add_pc2line_info(s, bc_out.size, line_num, col_num); dbuf_putc(&bc_out, (cc.op == OP_inc || cc.op == OP_post_inc) && - (s->vars[idx].is_safe || s->vars[idx].is_i32_inferred) + (sx_is_safe_i32(&s->vars[idx]) || s->vars[idx].is_i32_inferred) ? OP_inc_loc_safe_i32 : ((cc.op == OP_inc || cc.op == OP_post_inc) ? OP_inc_loc : OP_dec_loc)); dbuf_putc(&bc_out, idx); @@ -39144,7 +39821,7 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) dbuf_putc(&bc_out, OP_push_atom_value); dbuf_put_u32(&bc_out, cc.atom); } - dbuf_putc(&bc_out, (s->vars[idx].is_safe || s->vars[idx].is_i32_inferred) ? OP_add_loc_safe_i32 : OP_add_loc); + dbuf_putc(&bc_out, (sx_is_safe_i32(&s->vars[idx]) || s->vars[idx].is_i32_inferred) ? OP_add_loc_safe_i32 : OP_add_loc); dbuf_putc(&bc_out, idx); pos_next = cc.pos; break; @@ -39157,7 +39834,7 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) if (cc.col_num >= 0) col_num = cc.col_num; add_pc2line_info(s, bc_out.size, line_num, col_num); push_short_int(&bc_out, cc.label); - dbuf_putc(&bc_out, (s->vars[idx].is_safe || s->vars[idx].is_i32_inferred) ? OP_add_loc_safe_i32 : OP_add_loc); + dbuf_putc(&bc_out, (sx_is_safe_i32(&s->vars[idx]) || s->vars[idx].is_i32_inferred) ? OP_add_loc_safe_i32 : OP_add_loc); dbuf_putc(&bc_out, idx); pos_next = cc.pos; break; @@ -39172,7 +39849,7 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) if (cc.col_num >= 0) col_num = cc.col_num; add_pc2line_info(s, bc_out.size, line_num, col_num); put_short_code(&bc_out, cc.op, cc.idx); - dbuf_putc(&bc_out, (s->vars[idx].is_safe || s->vars[idx].is_i32_inferred) ? OP_add_loc_safe_i32 : OP_add_loc); + dbuf_putc(&bc_out, (sx_is_safe_i32(&s->vars[idx]) || s->vars[idx].is_i32_inferred) ? OP_add_loc_safe_i32 : OP_add_loc); dbuf_putc(&bc_out, idx); pos_next = cc.pos; break; @@ -39185,7 +39862,7 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) arg slot; check the trailing byte directly instead of trying to pack six alternatives into one mask. */ - if (idx < 256 && (s->vars[idx].is_safe || s->vars[idx].is_i32_inferred)) { + if (idx < 256 && (sx_is_safe_i32(&s->vars[idx]) || s->vars[idx].is_i32_inferred)) { bool rhs_is_push_i32 = code_match(&cc, pos_next, OP_push_i32, -1); bool rhs_matched = rhs_is_push_i32 || code_match(&cc, pos_next, M3(OP_get_loc, OP_get_arg, OP_get_var_ref), -1, -1); @@ -39867,6 +40544,9 @@ static JSValue js_create_function(JSContext *ctx, JSFunctionDef *fd) if (fuse_i32_accum_loops(ctx, fd)) goto fail; + if (sx_inline_typed_calls(ctx, fd)) + goto fail; + #ifdef ENABLE_DUMPS // JS_DUMP_BYTECODE_PASS2 if (check_dump_flag(ctx->rt, JS_DUMP_BYTECODE_PASS2)) { printf("pass 2\n"); @@ -39908,6 +40588,7 @@ static JSValue js_create_function(JSContext *ctx, JSFunctionDef *fd) fd->byte_code.buf = NULL; b->func_name = fd->func_name; + b->sx_ret_type = fd->sx_ret_type; if (fd->arg_count + fd->var_count > 0) { b->vardefs = (void *)((uint8_t*)b + vardefs_offset); if (fd->arg_count > 0) @@ -40494,8 +41175,16 @@ static __exception int js_parse_function_decl2(JSParseState *s, and an absent argument is already undefined here. */ if (s->token.val == '?' && next_token(s)) goto fail; - if (s->token.val == ':' && js_parse_type_annotation(s, false)) - goto fail; + if (s->token.val == ':') { + SxType declared; + if (js_parse_type_annotation(s, false, &declared)) + goto fail; + /* fd->args is a JSVarDef[], so a parameter carries its + declared type in the same field a local does, and it + reaches JSFunctionBytecode->vardefs by the memcpy in + js_create_function. */ + fd->args[idx].sx_type = declared; + } if (rest) { emit_op(s, OP_rest); emit_u16(s, idx); @@ -40611,8 +41300,10 @@ static __exception int js_parse_function_decl2(JSParseState *s, type (`(...): T => ...` / `(...): T {`) is compile-time metadata, skip it while leaving the token positioned at the body. */ if (s->token.val == ':') { - if (js_parse_type_annotation(s, true)) + SxType declared; + if (js_parse_type_annotation(s, true, &declared)) goto fail; + fd->sx_ret_type = declared; } /* generator function: yield after the parameters are evaluated */ @@ -53622,6 +54313,37 @@ static void json_free_parse_record(JSContext *ctx, JSONParseRecord *pr) } /* 'pr' can be NULL */ +/* The atom for a JSON property name, remembering the last few. The pointer + is into the document being parsed, which outlives the parse, so a hit is + confirmed with a compare of the bytes themselves. */ +static JSAtom json_key_atom(JSParseState *s, const uint8_t *p, uint32_t len) +{ + uint32_t slot = (len * 31u + (len ? p[0] : 0u)) & 3u; + JSAtom atom = s->json_keys[slot].atom; + + if (atom != JS_ATOM_NULL && s->json_keys[slot].len == len + && !memcmp(s->json_keys[slot].ptr, p, len)) + return JS_DupAtom(s->ctx, atom); + atom = JS_NewAtomLen(s->ctx, (const char *)p, len); + if (atom == JS_ATOM_NULL) + return JS_ATOM_NULL; + if (s->json_keys[slot].atom != JS_ATOM_NULL) + JS_FreeAtom(s->ctx, s->json_keys[slot].atom); + s->json_keys[slot].ptr = p; + s->json_keys[slot].len = len; + s->json_keys[slot].atom = JS_DupAtom(s->ctx, atom); + return atom; +} + +static void json_free_key_cache(JSParseState *s) +{ + uint32_t i; + for (i = 0; i < countof(s->json_keys); i++) { + JS_FreeAtom(s->ctx, s->json_keys[i].atom); + s->json_keys[i].atom = JS_ATOM_NULL; + } +} + static JSValue json_parse_value(JSParseState *s, JSONParseRecord *pr) { JSContext *ctx = s->ctx; @@ -53656,7 +54378,11 @@ static JSValue json_parse_value(JSParseState *s, JSONParseRecord *pr) if (s->token.val != '}') { for(;;) { if (s->token.val == TOK_STRING) { - prop_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (s->token.u.str.raw) + prop_name = json_key_atom(s, s->token.u.str.raw, + s->token.u.str.raw_len); + else + prop_name = JS_ValueToAtom(ctx, s->token.u.str.str); if (prop_name == JS_ATOM_NULL) goto fail; } else { @@ -53684,8 +54410,29 @@ static JSValue json_parse_value(JSParseState *s, JSONParseRecord *pr) JS_FreeAtom(ctx, prop_name); goto fail; } - ret = JS_DefinePropertyValue(ctx, val, prop_name, - prop_val, JS_PROP_C_W_E); + /* The object was created by JS_NewObject just above and + nothing but this loop can see it, so an own data + property goes in without the descriptor machinery. + A key that repeats -- legal in JSON, last one wins -- + is the only case that finds an existing slot. */ + { + JSObject *po = JS_VALUE_GET_OBJ(val); + JSProperty *pr1; + JSShapeProperty *prs1 = find_own_property(&pr1, po, prop_name); + if (prs1) { + set_value(ctx, &pr1->u.value, prop_val); + ret = 0; + } else { + pr1 = add_property(ctx, po, prop_name, JS_PROP_C_W_E); + if (!pr1) { + JS_FreeValue(ctx, prop_val); + ret = -1; + } else { + pr1->u.value = prop_val; + ret = 0; + } + } + } JS_FreeAtom(ctx, prop_name); if (ret < 0) goto fail; @@ -53734,7 +54481,11 @@ static JSValue json_parse_value(JSParseState *s, JSONParseRecord *pr) el = json_parse_value(s, pr1); if (JS_IsException(el)) goto fail; - ret = JS_DefinePropertyValueUint32(ctx, val, idx, el, JS_PROP_C_W_E); + /* Appending to the fresh fast array this loop is + filling, rather than defining an indexed property on + an object that might be anything. */ + ret = add_fast_array_element(ctx, JS_VALUE_GET_OBJ(val), + el, JS_PROP_THROW); if (ret < 0) goto fail; if (s->token.val == ']') @@ -53752,7 +54503,14 @@ static JSValue json_parse_value(JSParseState *s, JSONParseRecord *pr) } break; case TOK_STRING: - val = js_dup(s->token.u.str.str); + if (s->token.u.str.raw) { + val = js_new_string8_len(ctx, (const char *)s->token.u.str.raw, + s->token.u.str.raw_len); + if (JS_IsException(val)) + goto fail; + } else { + val = js_dup(s->token.u.str.str); + } if (pr) { json_parse_record_init_primitive(ctx, pr, val, s->token.ptr - s->buf_start, @@ -53820,6 +54578,7 @@ static JSValue JS_ParseJSON_internal(JSContext *ctx, const char *buf, size_t buf if (json_next_token(s)) goto fail; val = json_parse_value(s, pr); + json_free_key_cache(s); if (JS_IsException(val)) goto fail; if (s->token.val != TOK_EOF) { @@ -54055,6 +54814,49 @@ static JSValue js_json_rawJSON(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; } +/* The property names of a plain object, straight from its shape into the + caller's array, when they are all ordinary string keys. JSON.stringify + runs this per object, and the general enumerator allocates an array and + classifies every key by kind first. Returns false -- having taken no + references -- when anything about the object is not ordinary, and the + caller falls back to the general path. */ +static bool json_collect_plain_keys(JSContext *ctx, JSObject *p, + JSPropertyEnum *tab, uint16_t *slots, + uint32_t size, uint32_t *plen) +{ + JSShape *sh = p->shape; + JSShapeProperty *prs; + uint32_t i, n = 0, num_key; + + if (sh->prop_count > size) + return false; + for(i = 0, prs = get_shape_prop(sh); i < sh->prop_count; i++, prs++) { + JSAtom atom = prs->atom; + if (atom == JS_ATOM_NULL) + continue; + /* A numeric key would have to be sorted ahead of the others, and a + symbol is not a JSON key at all: leave both to the general path, + along with anything that is not a plain stored value -- a getter + has to be called, and the caller reads the slot directly. */ + if (JS_AtomGetKind(ctx, atom) != JS_ATOM_KIND_STRING + || JS_AtomIsArrayIndex(ctx, &num_key, atom)) + return false; + if ((prs->flags & JS_PROP_TMASK) != JS_PROP_NORMAL) + return false; + /* Enumerability is decided here, once, the way the specification's + EnumerableOwnPropertyNames decides it -- not again per property + after a getter has had the chance to change it. */ + if (!(prs->flags & JS_PROP_ENUMERABLE)) + continue; + tab[n].atom = JS_DupAtom(ctx, atom); + tab[n].is_enumerable = true; + slots[n] = (uint16_t)i; + n++; + } + *plen = n; + return true; +} + typedef struct JSONStringifyContext { JSValueConst replacer_func; JSValue stack; @@ -54062,8 +54864,47 @@ typedef struct JSONStringifyContext { JSValue gap; JSValue empty; StringBuffer *b; + /* The last shape found to have no `toJSON` anywhere on its prototype + chain, valid while the runtime's property-location generation is + unchanged. A document is thousands of objects of a handful of shapes, + and each one otherwise walks its prototype chain for a property that + is not there. */ + JSShape *no_tojson_shape; + uint32_t no_tojson_gen; + /* The objects on the path from the root, for the circular-reference + check. It used to be a JavaScript array, so every object cost an + Array#includes, a push and a pop through the generic property paths. + Borrowed pointers: an entry is on the path only while the frame + holding a reference to it is running. */ + JSObject **path; + int path_len, path_size; } JSONStringifyContext; +/* Is `p` already on the path from the root -- that is, a cycle? The path is + a few entries deep in any real document. */ +static bool json_path_has(JSONStringifyContext *jsc, JSObject *p) +{ + int i; + for (i = 0; i < jsc->path_len; i++) + if (jsc->path[i] == p) + return true; + return false; +} + +static int json_path_push(JSContext *ctx, JSONStringifyContext *jsc, JSObject *p) +{ + if (jsc->path_len >= jsc->path_size) { + int size = jsc->path_size ? jsc->path_size * 2 : 16; + JSObject **path = js_realloc(ctx, jsc->path, sizeof(*path) * size); + if (!path) + return -1; + jsc->path = path; + jsc->path_size = size; + } + jsc->path[jsc->path_len++] = p; + return 0; +} + static JSValue JS_ToQuotedStringFree(JSContext *ctx, JSValue val) { JSValue r = JS_ToQuotedString(ctx, val); JS_FreeValue(ctx, val); @@ -54078,9 +54919,33 @@ static JSValue js_json_check(JSContext *ctx, JSONStringifyContext *jsc, JSValueConst args[2]; if (JS_IsObject(val) || JS_IsBigInt(val)) { - JSValue f = JS_GetProperty(ctx, val, JS_ATOM_toJSON); + JSValue f; + JSObject *o = JS_IsObject(val) ? JS_VALUE_GET_OBJ(val) : NULL; + if (o && o->shape == jsc->no_tojson_shape + && jsc->no_tojson_gen == ctx->rt->prop_cache_gen) + goto no_tojson; + f = JS_GetProperty(ctx, val, JS_ATOM_toJSON); if (JS_IsException(f)) goto exception; + /* Only when nothing on the chain is exotic -- every Proxy shares one + shape and answers from its own trap -- and nothing on it carries a + `toJSON` property at all. A shape records which properties exist, + not what they hold, so a sibling object of the same shape can have a + function where this one had undefined, and storing a value does not + move it and so does not bump the generation. */ + if (o && JS_IsUndefined(f) && !o->is_exotic) { + JSObject *chain = o; + JSProperty *pr1; + while (!chain->is_exotic + && !find_own_property(&pr1, chain, JS_ATOM_toJSON)) { + chain = chain->shape->proto; + if (chain == NULL) { + jsc->no_tojson_shape = o->shape; + jsc->no_tojson_gen = ctx->rt->prop_cache_gen; + break; + } + } + } if (JS_IsFunction(ctx, f)) { v = JS_CallFree(ctx, f, val, 1, &key); JS_FreeValue(ctx, val); @@ -54091,6 +54956,7 @@ static JSValue js_json_check(JSContext *ctx, JSONStringifyContext *jsc, JS_FreeValue(ctx, f); } } + no_tojson: if (!JS_IsUndefined(jsc->replacer_func)) { args[0] = key; @@ -54133,9 +54999,16 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, { JSValue indent1, sep, sep1, tab, v, prop; JSObject *p; + JSPropertyEnum *atoms = NULL, stack_atoms[16]; + uint16_t stack_slots[16]; + JSShape *keys_shape = NULL; + uint32_t keys_gen = 0, atom_count = 0; int64_t i, len; int cl, ret; bool has_content; + /* With no gap both separators are the empty string, and every one of + these is a call that copies nothing. */ + bool indented = !JS_IsEmptyString(jsc->gap); indent1 = JS_UNDEFINED; sep = JS_UNDEFINED; @@ -54173,10 +55046,7 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, val = val1; goto concat_value; } - v = js_array_includes(ctx, jsc->stack, 1, vc(&val)); - if (JS_IsException(v)) - goto exception; - if (JS_ToBoolFree(ctx, v)) { + if (json_path_has(jsc, JS_VALUE_GET_OBJ(val))) { JS_ThrowTypeError(ctx, "circular reference"); goto exception; } @@ -54194,8 +55064,7 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, sep = js_dup(jsc->empty); sep1 = js_dup(jsc->empty); } - v = js_array_push(ctx, jsc->stack, 1, vc(&val), 0); - if (check_exception_free(ctx, v)) + if (json_path_push(ctx, jsc, JS_VALUE_GET_OBJ(val))) goto exception; ret = js_is_array(ctx, val); if (ret < 0) @@ -54203,11 +55072,12 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, if (ret) { if (js_get_length64(ctx, &len, val)) goto exception; - string_buffer_putc8(jsc->b, '['); + string_buffer_putc8_fast(jsc->b, '['); for(i = 0; i < len; i++) { if (i > 0) - string_buffer_putc8(jsc->b, ','); - string_buffer_concat_value(jsc->b, sep); + string_buffer_putc8_fast(jsc->b, ','); + if (indented) + string_buffer_concat_value(jsc->b, sep); v = JS_GetPropertyInt64(ctx, val, i); if (JS_IsException(v)) goto exception; @@ -54226,28 +55096,92 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, goto exception; } if (len > 0 && !JS_IsEmptyString(jsc->gap)) { - string_buffer_putc8(jsc->b, '\n'); + string_buffer_putc8_fast(jsc->b, '\n'); string_buffer_concat_value(jsc->b, indent); } - string_buffer_putc8(jsc->b, ']'); + string_buffer_putc8_fast(jsc->b, ']'); } else { - if (!JS_IsUndefined(jsc->property_list)) - tab = js_dup(jsc->property_list); - else - tab = js_object_keys(ctx, JS_UNDEFINED, 1, vc(&val), - JS_ITERATOR_KIND_KEY); - if (JS_IsException(tab)) - goto exception; - if (js_get_length64(ctx, &len, tab)) - goto exception; - string_buffer_putc8(jsc->b, '{'); + /* An ordinary object with no replacer list: walk its own atoms + rather than building an array of key strings and looking each + one back up by string. Same enumeration -- own string keys, + enumerable when read -- from the same place js_object_keys + takes it. */ + if (JS_IsUndefined(jsc->property_list) + && JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT + && JS_VALUE_GET_OBJ(val)->class_id == JS_CLASS_OBJECT + && !JS_VALUE_GET_OBJ(val)->is_exotic) { + if (json_collect_plain_keys(ctx, JS_VALUE_GET_OBJ(val), + stack_atoms, stack_slots, + countof(stack_atoms), &atom_count)) { + atoms = stack_atoms; + keys_shape = JS_VALUE_GET_OBJ(val)->shape; + keys_gen = ctx->rt->prop_cache_gen; + } else if (JS_GetOwnPropertyNamesInternal(ctx, &atoms, &atom_count, + JS_VALUE_GET_OBJ(val), + JS_GPN_STRING_MASK)) { + goto exception; + } else { + /* Drop what is not enumerable now, before any getter can + run: the list js_object_keys would have built is a + snapshot, and so is this one. */ + uint32_t k = 0, j; + for (j = 0; j < atom_count; j++) { + int desc_flags, res; + res = JS_GetOwnPropertyFlagsInternal(ctx, &desc_flags, + JS_VALUE_GET_OBJ(val), + atoms[j].atom); + if (res < 0) { + for (; j < atom_count; j++) + JS_FreeAtom(ctx, atoms[j].atom); + atom_count = k; + goto exception; + } + if (!res || !(desc_flags & JS_PROP_ENUMERABLE)) { + JS_FreeAtom(ctx, atoms[j].atom); + continue; + } + atoms[k++] = atoms[j]; + } + atom_count = k; + } + len = atom_count; + } else { + if (!JS_IsUndefined(jsc->property_list)) + tab = js_dup(jsc->property_list); + else + tab = js_object_keys(ctx, JS_UNDEFINED, 1, vc(&val), + JS_ITERATOR_KIND_KEY); + if (JS_IsException(tab)) + goto exception; + if (js_get_length64(ctx, &len, tab)) + goto exception; + } + string_buffer_putc8_fast(jsc->b, '{'); has_content = false; for(i = 0; i < len; i++) { JS_FreeValue(ctx, prop); - prop = JS_GetPropertyInt64(ctx, tab, i); - if (JS_IsException(prop)) - goto exception; - v = JS_GetPropertyValue(ctx, val, js_dup(prop)); + if (atoms) { + JSAtom atom = atoms[i].atom; + JSObject *po = JS_VALUE_GET_OBJ(val); + /* Nothing has moved since the keys were collected -- same + shape, and the runtime's property-location generation + has not been bumped -- so the slot recorded then is + still this property's. */ + if (po->shape == keys_shape && ctx->rt->prop_cache_gen == keys_gen) + v = js_dup(po->prop[stack_slots[i]].u.value); + else + v = JS_GetProperty(ctx, val, atom); + prop = JS_AtomToValue(ctx, atom); + if (JS_IsException(prop)) { + JS_FreeValue(ctx, v); + goto exception; + } + } else { + prop = JS_GetPropertyInt64(ctx, tab, i); + if (JS_IsException(prop)) + goto exception; + v = JS_GetPropertyValue(ctx, val, js_dup(prop)); + } if (JS_IsException(v)) goto exception; v = js_json_check(ctx, jsc, val, v, prop); @@ -54255,29 +55189,38 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, goto exception; if (!JS_IsUndefined(v)) { if (has_content) - string_buffer_putc8(jsc->b, ','); - prop = JS_ToQuotedStringFree(ctx, prop); - if (JS_IsException(prop)) { + string_buffer_putc8_fast(jsc->b, ','); + if (indented) + if (indented) + string_buffer_concat_value(jsc->b, sep); + if (string_buffer_quote(jsc->b, prop)) { JS_FreeValue(ctx, v); goto exception; } - string_buffer_concat_value(jsc->b, sep); - string_buffer_concat_value(jsc->b, prop); - string_buffer_putc8(jsc->b, ':'); - string_buffer_concat_value(jsc->b, sep1); + string_buffer_putc8_fast(jsc->b, ':'); + if (indented) + string_buffer_concat_value(jsc->b, sep1); if (js_json_to_str(ctx, jsc, val, v, indent1)) goto exception; has_content = true; } } if (has_content && JS_VALUE_GET_STRING(jsc->gap)->len != 0) { - string_buffer_putc8(jsc->b, '\n'); + string_buffer_putc8_fast(jsc->b, '\n'); string_buffer_concat_value(jsc->b, indent); } - string_buffer_putc8(jsc->b, '}'); + string_buffer_putc8_fast(jsc->b, '}'); + if (atoms) { + if (atoms == stack_atoms) { + for (i = 0; i < atom_count; i++) + JS_FreeAtom(ctx, atoms[i].atom); + } else { + js_free_prop_enum(ctx, atoms, atom_count); + } + atoms = NULL; + } } - if (check_exception_free(ctx, js_array_pop(ctx, jsc->stack, 0, NULL, 0))) - goto exception; + jsc->path_len--; JS_FreeValue(ctx, val); JS_FreeValue(ctx, tab); JS_FreeValue(ctx, sep); @@ -54290,10 +55233,9 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, switch (JS_VALUE_GET_NORM_TAG(val)) { case JS_TAG_STRING: case JS_TAG_STRING_ROPE: - val = JS_ToQuotedStringFree(ctx, val); - if (JS_IsException(val)) - goto exception; - goto concat_value; + ret = string_buffer_quote(jsc->b, val); + JS_FreeValue(ctx, val); + return ret; case JS_TAG_FLOAT64: if (!isfinite(JS_VALUE_GET_FLOAT64(val))) { val = JS_NULL; @@ -54314,6 +55256,12 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, } exception: + if (atoms == stack_atoms) { + for (i = 0; i < atom_count; i++) + JS_FreeAtom(ctx, atoms[i].atom); + } else if (atoms) { + js_free_prop_enum(ctx, atoms, atom_count); + } JS_FreeValue(ctx, val); JS_FreeValue(ctx, tab); JS_FreeValue(ctx, sep); @@ -54333,7 +55281,12 @@ JSValue JS_JSONStringify(JSContext *ctx, JSValueConst obj, int64_t i, j, n; jsc->replacer_func = JS_UNDEFINED; + jsc->no_tojson_shape = NULL; + jsc->no_tojson_gen = 0; jsc->stack = JS_UNDEFINED; + jsc->path = NULL; + jsc->path_len = 0; + jsc->path_size = 0; jsc->property_list = JS_UNDEFINED; jsc->gap = JS_UNDEFINED; jsc->b = &b_s; @@ -54458,6 +55411,7 @@ JSValue JS_JSONStringify(JSContext *ctx, JSValueConst obj, JS_FreeValue(ctx, jsc->gap); JS_FreeValue(ctx, jsc->property_list); JS_FreeValue(ctx, jsc->stack); + js_free(ctx, jsc->path); return ret; } diff --git a/tooling/vscode/package.json b/tooling/vscode/package.json index 5b9367a..ca53184 100644 --- a/tooling/vscode/package.json +++ b/tooling/vscode/package.json @@ -1,7 +1,7 @@ { "name": "sxfescript", "displayName": "SxfeScript", - "version": "0.0.1", + "version": "0.0.2", "publisher": "raythings", "engines": { "vscode": "^1.90.0" }, "categories": ["Programming Languages"],