From 5e2047f3a1bec0d25f4eeb6778826dd408b16db7 Mon Sep 17 00:00:00 2001 From: Thomas Aubry Date: Mon, 20 Apr 2026 22:59:45 +0200 Subject: [PATCH 1/7] Add Zig 0.16 port of the CLI spec parser and URL builder Narrow Zig parallel to the Rust ItemSpec::parse and fetch::build_url, kept minimal so a later wasm target can be compared against a same-scope Rust wasm build. Network, zstd, and rendering stay in the Rust crate. https: //claude.ai/code/session_01Ei65b8fDC7sABmKnq1Wgne Co-authored-by: Claude --- .gitignore | 2 + zig/README.md | 36 +++++++++++ zig/build.zig | 38 ++++++++++++ zig/build.zig.zon | 12 ++++ zig/src/main.zig | 92 ++++++++++++++++++++++++++++ zig/src/spec.zig | 153 ++++++++++++++++++++++++++++++++++++++++++++++ zig/src/url.zig | 78 +++++++++++++++++++++++ 7 files changed, 411 insertions(+) create mode 100644 zig/README.md create mode 100644 zig/build.zig create mode 100644 zig/build.zig.zon create mode 100644 zig/src/main.zig create mode 100644 zig/src/spec.zig create mode 100644 zig/src/url.zig diff --git a/.gitignore b/.gitignore index ea8c4bf..ceacb48 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +zig/zig-out/ +zig/.zig-cache/ diff --git a/zig/README.md b/zig/README.md new file mode 100644 index 0000000..537d475 --- /dev/null +++ b/zig/README.md @@ -0,0 +1,36 @@ +# md-docrs-zig + +Zig 0.16 port of the CLI-facing portion of `md-docrs-proxy`. Scope is deliberately narrow: + +- Parse the `crate[@version][::path::to::item]` spec grammar (`src/spec.zig`). +- Build the docs.rs rustdoc JSON URL (`src/url.zig`). +- Drive both from a CLI (`src/main.zig`). + +Network fetch, zstd decode, rustdoc-JSON deserialisation and Markdown rendering still live in the Rust crate at the repo root. Keeping the Zig binary this small lets us produce a WebAssembly module that is meaningful to compare against the Rust build. + +## Build + +```sh +cd zig +zig build # debug binary at zig-out/bin/md-docrs-zig +zig build -Doptimize=ReleaseSmall +zig build test +zig build run -- serde::de::Deserialize +``` + +## Example + +```sh +$ ./zig-out/bin/md-docrs-zig tokio@1.52.1::sync::Mutex --target x86_64-unknown-linux-gnu +https://docs.rs/crate/tokio/1.52.1/x86_64-unknown-linux-gnu/json/57.zst +``` + +## WebAssembly + +The WASM target will be wired up next. The intended invocation is: + +```sh +zig build -Dtarget=wasm32-wasi -Doptimize=ReleaseSmall +``` + +At that point we'll add a matching `wasm32-wasip1` (or `wasm32-unknown-unknown`) target to the Rust crate and compare `.wasm` sizes for the same surface area (spec parsing + URL building). diff --git a/zig/build.zig b/zig/build.zig new file mode 100644 index 0000000..4795ac9 --- /dev/null +++ b/zig/build.zig @@ -0,0 +1,38 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const root_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "md-docrs-zig", + .root_module = root_mod, + }); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + + const run_step = b.step("run", "Run the CLI"); + run_step.dependOn(&run_cmd.step); + + const test_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const unit_tests = b.addTest(.{ .root_module = test_mod }); + const run_tests = b.addRunArtifact(unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_tests.step); +} diff --git a/zig/build.zig.zon b/zig/build.zig.zon new file mode 100644 index 0000000..6b53609 --- /dev/null +++ b/zig/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .md_docrs_zig, + .version = "0.1.0", + .fingerprint = 0xb3f2a91c4d6e7058, + .minimum_zig_version = "0.16.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/src/main.zig b/zig/src/main.zig new file mode 100644 index 0000000..f3f7541 --- /dev/null +++ b/zig/src/main.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const spec_mod = @import("spec.zig"); +const url_mod = @import("url.zig"); + +const ItemSpec = spec_mod.ItemSpec; + +const usage = + \\usage: md-docrs-zig [--target TRIPLE] + \\ + \\Spec grammar: crate[@version][::path::to::item] + \\ + \\v0 prints the rustdoc JSON URL that the Rust implementation would fetch. + \\The full fetch + render pipeline lives in the Rust crate; this binary is + \\kept lean so it can be compiled to WebAssembly for a size comparison. + \\ +; + +pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + var stdout_buf: [4096]u8 = undefined; + var stdout_file = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_file.interface; + + var stderr_buf: [4096]u8 = undefined; + var stderr_file = std.fs.File.stderr().writer(&stderr_buf); + const stderr = &stderr_file.interface; + + var spec_arg: ?[]const u8 = null; + var target: ?[]const u8 = null; + + var i: usize = 1; + while (i < args.len) : (i += 1) { + const a = args[i]; + if (std.mem.eql(u8, a, "--target")) { + i += 1; + if (i >= args.len) { + try stderr.writeAll("error: --target requires a value\n"); + try stderr.flush(); + std.process.exit(2); + } + target = args[i]; + } else if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "--help")) { + try stdout.writeAll(usage); + try stdout.flush(); + return; + } else if (spec_arg == null) { + spec_arg = a; + } else { + try stderr.print("error: unexpected argument: {s}\n", .{a}); + try stderr.flush(); + std.process.exit(2); + } + } + + const raw = spec_arg orelse { + try stderr.writeAll(usage); + try stderr.flush(); + std.process.exit(2); + }; + + var spec = ItemSpec.parse(allocator, raw) catch |err| { + try stderr.print("invalid spec '{s}': {s}\n", .{ raw, @errorName(err) }); + try stderr.flush(); + std.process.exit(2); + }; + defer spec.deinit(); + spec.target = target; + + const url = try url_mod.buildUrl( + allocator, + url_mod.DEFAULT_BASE, + spec.crate_name, + spec.version, + spec.target, + url_mod.FORMAT_VERSION, + ); + defer allocator.free(url); + + try stdout.print("{s}\n", .{url}); + try stdout.flush(); +} + +test { + _ = @import("spec.zig"); + _ = @import("url.zig"); +} diff --git a/zig/src/spec.zig b/zig/src/spec.zig new file mode 100644 index 0000000..d357ccd --- /dev/null +++ b/zig/src/spec.zig @@ -0,0 +1,153 @@ +const std = @import("std"); + +pub const ParseError = error{ + Empty, + BadCrateVersion, + BadCrateName, + BadPathSegment, + OutOfMemory, +}; + +/// Parsed `crate[@version][::path::to::item]` reference. +/// Field slices borrow from the `raw` input passed to `parse`; keep it alive. +pub const ItemSpec = struct { + crate_name: []const u8, + version: []const u8, + target: ?[]const u8 = null, + path: [][]const u8, + allocator: std.mem.Allocator, + + pub fn deinit(self: *ItemSpec) void { + self.allocator.free(self.path); + } + + pub fn isRoot(self: *const ItemSpec) bool { + return self.path.len == 0; + } + + pub fn parse(allocator: std.mem.Allocator, raw: []const u8) ParseError!ItemSpec { + const trimmed = std.mem.trim(u8, raw, &std.ascii.whitespace); + if (trimmed.len == 0) return ParseError.Empty; + + var head: []const u8 = trimmed; + var rest: []const u8 = ""; + if (std.mem.indexOf(u8, trimmed, "::")) |i| { + head = trimmed[0..i]; + rest = trimmed[i + 2 ..]; + } + + var path_list: std.ArrayList([]const u8) = .empty; + errdefer path_list.deinit(allocator); + + if (rest.len > 0) { + var it = std.mem.splitSequence(u8, rest, "::"); + while (it.next()) |seg| { + if (!isValidIdent(seg)) return ParseError.BadPathSegment; + try path_list.append(allocator, seg); + } + } + + var crate_name: []const u8 = head; + var version: []const u8 = "latest"; + if (std.mem.indexOfScalar(u8, head, '@')) |i| { + const c = head[0..i]; + const v = head[i + 1 ..]; + if (c.len == 0 or v.len == 0) return ParseError.BadCrateVersion; + crate_name = c; + version = v; + } + + if (!isValidCrateName(crate_name)) return ParseError.BadCrateName; + + return ItemSpec{ + .crate_name = crate_name, + .version = version, + .path = try path_list.toOwnedSlice(allocator), + .allocator = allocator, + }; + } +}; + +const MAX_CRATE_NAME_LEN: usize = 64; + +fn isValidCrateName(s: []const u8) bool { + if (s.len == 0 or s.len > MAX_CRATE_NAME_LEN) return false; + if (!std.ascii.isAlphabetic(s[0])) return false; + for (s[1..]) |c| { + if (!(std.ascii.isAlphanumeric(c) or c == '-' or c == '_')) return false; + } + return true; +} + +fn isValidIdent(s: []const u8) bool { + if (s.len == 0) return false; + for (s) |c| { + if (!(std.ascii.isAlphanumeric(c) or c == '_')) return false; + } + return true; +} + +test "bare crate" { + const a = std.testing.allocator; + var s = try ItemSpec.parse(a, "serde"); + defer s.deinit(); + try std.testing.expectEqualStrings("serde", s.crate_name); + try std.testing.expectEqualStrings("latest", s.version); + try std.testing.expect(s.isRoot()); +} + +test "crate with version" { + const a = std.testing.allocator; + var s = try ItemSpec.parse(a, "serde@1.0.200"); + defer s.deinit(); + try std.testing.expectEqualStrings("1.0.200", s.version); +} + +test "crate with path" { + const a = std.testing.allocator; + var s = try ItemSpec.parse(a, "serde::de::Deserialize"); + defer s.deinit(); + try std.testing.expectEqualStrings("serde", s.crate_name); + try std.testing.expectEqual(@as(usize, 2), s.path.len); + try std.testing.expectEqualStrings("de", s.path[0]); + try std.testing.expectEqualStrings("Deserialize", s.path[1]); +} + +test "crate with version and path" { + const a = std.testing.allocator; + var s = try ItemSpec.parse(a, "anyhow@~1::Error"); + defer s.deinit(); + try std.testing.expectEqualStrings("anyhow", s.crate_name); + try std.testing.expectEqualStrings("~1", s.version); + try std.testing.expectEqual(@as(usize, 1), s.path.len); + try std.testing.expectEqualStrings("Error", s.path[0]); +} + +test "rejects empty" { + try std.testing.expectError(ParseError.Empty, ItemSpec.parse(std.testing.allocator, "")); +} + +test "rejects bad at" { + try std.testing.expectError(ParseError.BadCrateVersion, ItemSpec.parse(std.testing.allocator, "@1.0")); + try std.testing.expectError(ParseError.BadCrateVersion, ItemSpec.parse(std.testing.allocator, "serde@")); +} + +test "rejects invalid crate names" { + const a = std.testing.allocator; + try std.testing.expectError(ParseError.BadCrateName, ItemSpec.parse(a, "1serde")); + try std.testing.expectError(ParseError.BadCrateName, ItemSpec.parse(a, "-serde")); + try std.testing.expectError(ParseError.BadCrateName, ItemSpec.parse(a, "_serde")); + try std.testing.expectError(ParseError.BadCrateName, ItemSpec.parse(a, "ser de")); + try std.testing.expectError(ParseError.BadCrateName, ItemSpec.parse(a, "ser.de")); + const long = "a" ** 65; + try std.testing.expectError(ParseError.BadCrateName, ItemSpec.parse(a, long)); +} + +test "accepts valid crate names" { + const a = std.testing.allocator; + const max_name = "a" ** 64; + inline for (.{ "serde", "serde_json", "tracing-subscriber", "a", max_name }) |name| { + var s = try ItemSpec.parse(a, name); + defer s.deinit(); + } +} diff --git a/zig/src/url.zig b/zig/src/url.zig new file mode 100644 index 0000000..bc7e114 --- /dev/null +++ b/zig/src/url.zig @@ -0,0 +1,78 @@ +const std = @import("std"); + +/// rustdoc JSON format version this build targets, matching the Rust crate's +/// `rustdoc_types::FORMAT_VERSION`. Bump when upgrading rustdoc-types. +pub const FORMAT_VERSION: u32 = 57; + +pub const DEFAULT_BASE: []const u8 = "https://docs.rs"; + +/// Mirrors `build_url` in src/fetch.rs. Caller owns the returned slice. +pub fn buildUrl( + allocator: std.mem.Allocator, + base: []const u8, + crate_name: []const u8, + version: []const u8, + target: ?[]const u8, + format_version: ?u32, +) std.mem.Allocator.Error![]u8 { + if (target) |t| { + if (format_version) |fv| { + return std.fmt.allocPrint( + allocator, + "{s}/crate/{s}/{s}/{s}/json/{d}.zst", + .{ base, crate_name, version, t, fv }, + ); + } + return std.fmt.allocPrint( + allocator, + "{s}/crate/{s}/{s}/{s}/json.zst", + .{ base, crate_name, version, t }, + ); + } + if (format_version) |fv| { + return std.fmt.allocPrint( + allocator, + "{s}/crate/{s}/{s}/json/{d}.zst", + .{ base, crate_name, version, fv }, + ); + } + return std.fmt.allocPrint( + allocator, + "{s}/crate/{s}/{s}/json.zst", + .{ base, crate_name, version }, + ); +} + +test "url basic" { + const a = std.testing.allocator; + const s = try buildUrl(a, DEFAULT_BASE, "serde", "latest", null, null); + defer a.free(s); + try std.testing.expectEqualStrings("https://docs.rs/crate/serde/latest/json.zst", s); +} + +test "url with target" { + const a = std.testing.allocator; + const s = try buildUrl(a, DEFAULT_BASE, "serde", "latest", "x86_64-pc-windows-msvc", null); + defer a.free(s); + try std.testing.expectEqualStrings( + "https://docs.rs/crate/serde/latest/x86_64-pc-windows-msvc/json.zst", + s, + ); +} + +test "url format pinned" { + const a = std.testing.allocator; + const s = try buildUrl(a, DEFAULT_BASE, "serde", "1.0.200", null, 57); + defer a.free(s); + try std.testing.expectEqualStrings("https://docs.rs/crate/serde/1.0.200/json/57.zst", s); +} + +test "url format pinned with target" { + const a = std.testing.allocator; + const s = try buildUrl(a, DEFAULT_BASE, "tokio", "1.52.1", "x86_64-unknown-linux-gnu", 57); + defer a.free(s); + try std.testing.expectEqualStrings( + "https://docs.rs/crate/tokio/1.52.1/x86_64-unknown-linux-gnu/json/57.zst", + s, + ); +} From d9d191579623bcb35038bc5df047d38d9c1c7e27 Mon Sep 17 00:00:00 2001 From: Thomas Aubry Date: Mon, 20 Apr 2026 22:59:45 +0200 Subject: [PATCH 2/7] Package Zig port as a WASM Cloudflare Worker (zigflare layout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructures zig/ into lib/ (Zig sources) + src/ (TypeScript worker), matching mattzcarey/zigflare. build.zig now produces a wasm32-freestanding ReleaseSmall artifact exporting alloc, free, and resolve_url; the native CLI moves to `zig build cli` and goes through the same resolveUrl core so both surfaces are in lockstep. The worker at src/index.ts loads md_docrs.wasm, marshals the spec and optional target triple across the WASM boundary, and returns the resolved docs.rs rustdoc JSON URL. No network / zstd / rendering yet — keeps the artifact directly comparable to an equivalent Rust wasm32 build. https: //claude.ai/code/session_01Ei65b8fDC7sABmKnq1Wgne Co-authored-by: Claude --- .gitignore | 8 ++- zig/README.md | 88 +++++++++++++++++++++++------ zig/build.zig | 38 ------------- zig/lib/build.zig | 65 +++++++++++++++++++++ zig/{ => lib}/build.zig.zon | 0 zig/{src/main.zig => lib/cli.zig} | 41 +++++--------- zig/lib/resolve.zig | 82 +++++++++++++++++++++++++++ zig/{src => lib}/spec.zig | 0 zig/{src => lib}/url.zig | 0 zig/lib/wasm.zig | 43 ++++++++++++++ zig/package.json | 18 ++++++ zig/src/index.ts | 93 +++++++++++++++++++++++++++++++ zig/src/md_docrs.wasm.d.ts | 3 + zig/tsconfig.json | 21 +++++++ zig/wrangler.jsonc | 10 ++++ 15 files changed, 424 insertions(+), 86 deletions(-) delete mode 100644 zig/build.zig create mode 100644 zig/lib/build.zig rename zig/{ => lib}/build.zig.zon (100%) rename zig/{src/main.zig => lib/cli.zig} (69%) create mode 100644 zig/lib/resolve.zig rename zig/{src => lib}/spec.zig (100%) rename zig/{src => lib}/url.zig (100%) create mode 100644 zig/lib/wasm.zig create mode 100644 zig/package.json create mode 100644 zig/src/index.ts create mode 100644 zig/src/md_docrs.wasm.d.ts create mode 100644 zig/tsconfig.json create mode 100644 zig/wrangler.jsonc diff --git a/.gitignore b/.gitignore index ceacb48..4cd393d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /target -zig/zig-out/ -zig/.zig-cache/ +zig/lib/zig-out/ +zig/lib/.zig-cache/ +zig/node_modules/ +zig/.wrangler/ +zig/worker-configuration.d.ts +zig/src/md_docrs.wasm diff --git a/zig/README.md b/zig/README.md index 537d475..fdc757a 100644 --- a/zig/README.md +++ b/zig/README.md @@ -1,36 +1,88 @@ # md-docrs-zig -Zig 0.16 port of the CLI-facing portion of `md-docrs-proxy`. Scope is deliberately narrow: +Zig 0.16 port of the spec-parsing / URL-building portion of `md-docrs-proxy`, compiled two ways: -- Parse the `crate[@version][::path::to::item]` spec grammar (`src/spec.zig`). -- Build the docs.rs rustdoc JSON URL (`src/url.zig`). -- Drive both from a CLI (`src/main.zig`). +- **WASM** (`wasm32-freestanding`, `ReleaseSmall`) — runs on Cloudflare Workers via `src/index.ts`. Layout and memory protocol mirror [zigflare](https://github.com/mattzcarey/zigflare). +- **Native CLI** — same core `resolve.resolveUrl`, wrapped with argv handling in `lib/cli.zig`. Useful for local iteration and for A/B testing against the Rust binary. -Network fetch, zstd decode, rustdoc-JSON deserialisation and Markdown rendering still live in the Rust crate at the repo root. Keeping the Zig binary this small lets us produce a WebAssembly module that is meaningful to compare against the Rust build. +Scope is intentionally narrow so the WASM artifact is directly comparable to a same-scope Rust WASM build: no HTTP, no zstd, no rustdoc-JSON parsing, no Markdown renderer — those stay in the root Rust crate. + +## Layout + +``` +zig/ +├── lib/ # Zig sources (build runs here) +│ ├── build.zig +│ ├── build.zig.zon +│ ├── spec.zig # pure: crate[@version][::path] grammar +│ ├── url.zig # pure: docs.rs URL builder +│ ├── resolve.zig # pure: spec + url glue, native tests +│ ├── wasm.zig # WASM entry: alloc / free / resolve_url +│ └── cli.zig # native CLI entry +├── src/ # Cloudflare Worker (TypeScript) +│ ├── index.ts +│ ├── md_docrs.wasm.d.ts +│ └── md_docrs.wasm # produced by `npm run build:wasm` +├── package.json +├── tsconfig.json +└── wrangler.jsonc +``` ## Build ```sh -cd zig -zig build # debug binary at zig-out/bin/md-docrs-zig -zig build -Doptimize=ReleaseSmall -zig build test -zig build run -- serde::de::Deserialize +# WASM only (default target) +cd zig/lib && zig build +# -> zig-out/bin/md-docrs.wasm + +# Native CLI +cd zig/lib && zig build cli +./zig-out/bin/md-docrs-zig serde::de::Deserialize + +# Tests (native, pull in spec.zig + url.zig + resolve.zig tests) +cd zig/lib && zig build test + +# Run CLI through the build system +cd zig/lib && zig build run -- tokio@1.52.1::sync::Mutex --target x86_64-unknown-linux-gnu ``` -## Example +## Worker ```sh -$ ./zig-out/bin/md-docrs-zig tokio@1.52.1::sync::Mutex --target x86_64-unknown-linux-gnu -https://docs.rs/crate/tokio/1.52.1/x86_64-unknown-linux-gnu/json/57.zst +cd zig +npm install +npm run build:wasm # builds lib/ and copies the wasm into src/ +npm run dev # wrangler dev on localhost +npm run deploy # wrangler deploy ``` -## WebAssembly - -The WASM target will be wired up next. The intended invocation is: +Endpoints: ```sh -zig build -Dtarget=wasm32-wasi -Doptimize=ReleaseSmall +curl localhost:8787/serde # latest +curl localhost:8787/tokio@1.52.1::sync::Mutex +curl 'localhost:8787/tokio::sync::Mutex?target=x86_64-unknown-linux-gnu' +curl 'localhost:8787/?spec=anyhow::Error' ``` -At that point we'll add a matching `wasm32-wasip1` (or `wasm32-unknown-unknown`) target to the Rust crate and compare `.wasm` sizes for the same surface area (spec parsing + URL building). +All three print the fully resolved `https://docs.rs/crate//[/]/json/57.zst` URL. + +## WASM ABI + +Exported from `lib/wasm.zig`: + +| Export | Signature | Notes | +| --- | --- | --- | +| `alloc` | `(len: u32) -> *u8` | Backed by `std.heap.wasm_allocator`. Returns 0 on OOM. | +| `free` | `(ptr: *u8, len: u32)` | Caller must pass the exact length passed to `alloc`. | +| `resolve_url` | `(spec_ptr, spec_len, target_ptr, target_len, out_ptr, out_cap) -> u32` | Returns bytes written, or 0 on bad spec / out-of-space. `target_len == 0` means "no target override". | + +Memory protocol notes in the zigflare [`doc/memory.md`](https://github.com/mattzcarey/zigflare/blob/main/doc/memory.md) apply verbatim: always recreate `Uint8Array` views *after* each `alloc`, since WASM memory growth detaches existing views. + +## Comparing with Rust WASM + +Next step is adding a `wasm32-unknown-unknown` target to the Rust crate that exposes the same `resolve_url` surface. We can then compare: + +- `.wasm` size (Rust with `panic=abort` + LTO + `wasm-opt` vs. Zig `ReleaseSmall` + `strip`). +- Instantiation + call latency in a Worker. +- Cold-start cost (wrangler measures this). diff --git a/zig/build.zig b/zig/build.zig deleted file mode 100644 index 4795ac9..0000000 --- a/zig/build.zig +++ /dev/null @@ -1,38 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const root_mod = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - const exe = b.addExecutable(.{ - .name = "md-docrs-zig", - .root_module = root_mod, - }); - - b.installArtifact(exe); - - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| run_cmd.addArgs(args); - - const run_step = b.step("run", "Run the CLI"); - run_step.dependOn(&run_cmd.step); - - const test_mod = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - const unit_tests = b.addTest(.{ .root_module = test_mod }); - const run_tests = b.addRunArtifact(unit_tests); - - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_tests.step); -} diff --git a/zig/lib/build.zig b/zig/lib/build.zig new file mode 100644 index 0000000..0e49b6a --- /dev/null +++ b/zig/lib/build.zig @@ -0,0 +1,65 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + // ── WASM target (Cloudflare Workers / any WebAssembly host) ───────────── + // + // Mirrors the zigflare recipe: wasm32-freestanding, ReleaseSmall, strip, + // explicit export list, no entry point. This is the artifact we compare + // against the Rust wasm build. + const wasm_mod = b.createModule(.{ + .root_source_file = b.path("wasm.zig"), + .target = b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }), + .optimize = .ReleaseSmall, + .strip = true, + }); + wasm_mod.export_symbol_names = &.{ "alloc", "free", "resolve_url" }; + + const wasm = b.addExecutable(.{ + .name = "md-docrs", + .root_module = wasm_mod, + }); + wasm.entry = .disabled; + + const install_wasm = b.addInstallArtifact(wasm, .{}); + b.getInstallStep().dependOn(&install_wasm.step); + + // ── Native CLI ────────────────────────────────────────────────────────── + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const cli_mod = b.createModule(.{ + .root_source_file = b.path("cli.zig"), + .target = target, + .optimize = optimize, + }); + const cli = b.addExecutable(.{ + .name = "md-docrs-zig", + .root_module = cli_mod, + }); + + // Install CLI under a named step so the default `zig build` only produces + // the WASM artifact — keeps `npm run build:wasm` focused. + const install_cli = b.addInstallArtifact(cli, .{}); + const cli_step = b.step("cli", "Build the native CLI"); + cli_step.dependOn(&install_cli.step); + + const run_cli = b.addRunArtifact(cli); + run_cli.step.dependOn(&install_cli.step); + if (b.args) |args| run_cli.addArgs(args); + const run_step = b.step("run", "Run the native CLI"); + run_step.dependOn(&run_cli.step); + + // ── Unit tests (native) ───────────────────────────────────────────────── + const test_mod = b.createModule(.{ + .root_source_file = b.path("resolve.zig"), + .target = target, + .optimize = optimize, + }); + const tests = b.addTest(.{ .root_module = test_mod }); + const run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_tests.step); +} diff --git a/zig/build.zig.zon b/zig/lib/build.zig.zon similarity index 100% rename from zig/build.zig.zon rename to zig/lib/build.zig.zon diff --git a/zig/src/main.zig b/zig/lib/cli.zig similarity index 69% rename from zig/src/main.zig rename to zig/lib/cli.zig index f3f7541..7c654f1 100644 --- a/zig/src/main.zig +++ b/zig/lib/cli.zig @@ -1,17 +1,17 @@ +/// Native CLI entry point. Thin wrapper over `resolve.resolveUrl` so the exact +/// same code path is exercised by `zig build run` and the WASM worker. const std = @import("std"); -const spec_mod = @import("spec.zig"); -const url_mod = @import("url.zig"); - -const ItemSpec = spec_mod.ItemSpec; +const resolve = @import("resolve.zig"); const usage = \\usage: md-docrs-zig [--target TRIPLE] \\ \\Spec grammar: crate[@version][::path::to::item] \\ - \\v0 prints the rustdoc JSON URL that the Rust implementation would fetch. - \\The full fetch + render pipeline lives in the Rust crate; this binary is - \\kept lean so it can be compiled to WebAssembly for a size comparison. + \\Prints the rustdoc JSON URL that the Rust implementation would fetch. + \\The full fetch + render pipeline lives in the Rust crate; this binary + \\stays lean so the same logic can ship as a WebAssembly worker (see + \\../src/index.ts) for a size comparison with the Rust WASM build. \\ ; @@ -64,29 +64,14 @@ pub fn main() !void { std.process.exit(2); }; - var spec = ItemSpec.parse(allocator, raw) catch |err| { - try stderr.print("invalid spec '{s}': {s}\n", .{ raw, @errorName(err) }); + var buf: [512]u8 = undefined; + const n = resolve.resolveUrl(allocator, raw, target, &buf); + if (n == 0) { + try stderr.print("error: could not resolve URL for '{s}'\n", .{raw}); try stderr.flush(); std.process.exit(2); - }; - defer spec.deinit(); - spec.target = target; - - const url = try url_mod.buildUrl( - allocator, - url_mod.DEFAULT_BASE, - spec.crate_name, - spec.version, - spec.target, - url_mod.FORMAT_VERSION, - ); - defer allocator.free(url); + } - try stdout.print("{s}\n", .{url}); + try stdout.print("{s}\n", .{buf[0..n]}); try stdout.flush(); } - -test { - _ = @import("spec.zig"); - _ = @import("url.zig"); -} diff --git a/zig/lib/resolve.zig b/zig/lib/resolve.zig new file mode 100644 index 0000000..34479e4 --- /dev/null +++ b/zig/lib/resolve.zig @@ -0,0 +1,82 @@ +/// Pure, allocator-driven core: parse a spec and emit the docs.rs URL. +/// Shared by the native CLI (`cli.zig`) and the WASM entry (`wasm.zig`). +const std = @import("std"); +const spec_mod = @import("spec.zig"); +const url_mod = @import("url.zig"); + +pub const DEFAULT_BASE = url_mod.DEFAULT_BASE; +pub const FORMAT_VERSION = url_mod.FORMAT_VERSION; + +/// Parse `raw_spec`, combine with `target`, write the resolved URL into `out`. +/// Returns the number of bytes written, or 0 on any failure (invalid spec, +/// OOM, or `out` too small). The output is *not* null-terminated. +pub fn resolveUrl( + allocator: std.mem.Allocator, + raw_spec: []const u8, + target: ?[]const u8, + out: []u8, +) u32 { + var spec = spec_mod.ItemSpec.parse(allocator, raw_spec) catch return 0; + defer spec.deinit(); + spec.target = target; + + const url = url_mod.buildUrl( + allocator, + DEFAULT_BASE, + spec.crate_name, + spec.version, + spec.target, + FORMAT_VERSION, + ) catch return 0; + defer allocator.free(url); + + if (url.len > out.len) return 0; + @memcpy(out[0..url.len], url); + return @intCast(url.len); +} + +// Pull spec.zig and url.zig tests into the `zig build test` run. +test { + std.testing.refAllDecls(@This()); + _ = @import("spec.zig"); + _ = @import("url.zig"); +} + +test "resolve bare crate" { + var buf: [256]u8 = undefined; + const n = resolveUrl(std.testing.allocator, "serde", null, &buf); + try std.testing.expectEqualStrings( + "https://docs.rs/crate/serde/latest/json/57.zst", + buf[0..n], + ); +} + +test "resolve pinned with target" { + var buf: [256]u8 = undefined; + const n = resolveUrl( + std.testing.allocator, + "tokio@1.52.1::sync::Mutex", + "x86_64-unknown-linux-gnu", + &buf, + ); + try std.testing.expectEqualStrings( + "https://docs.rs/crate/tokio/1.52.1/x86_64-unknown-linux-gnu/json/57.zst", + buf[0..n], + ); +} + +test "resolve invalid spec returns zero" { + var buf: [256]u8 = undefined; + try std.testing.expectEqual( + @as(u32, 0), + resolveUrl(std.testing.allocator, "1bad", null, &buf), + ); +} + +test "resolve output buffer too small returns zero" { + var buf: [8]u8 = undefined; + try std.testing.expectEqual( + @as(u32, 0), + resolveUrl(std.testing.allocator, "serde", null, &buf), + ); +} diff --git a/zig/src/spec.zig b/zig/lib/spec.zig similarity index 100% rename from zig/src/spec.zig rename to zig/lib/spec.zig diff --git a/zig/src/url.zig b/zig/lib/url.zig similarity index 100% rename from zig/src/url.zig rename to zig/lib/url.zig diff --git a/zig/lib/wasm.zig b/zig/lib/wasm.zig new file mode 100644 index 0000000..b582adb --- /dev/null +++ b/zig/lib/wasm.zig @@ -0,0 +1,43 @@ +/// WASM entry point. Exports `alloc`, `free`, and `resolve_url` for use from +/// a Cloudflare Worker (or any other WebAssembly host). +/// +/// Memory protocol mirrors zigflare: +/// 1. Host calls `alloc(n)` to reserve input/output buffers inside WASM memory. +/// 2. Host writes input bytes into those buffers from JS. +/// 3. Host calls `resolve_url(...)`, which returns the number of bytes written +/// to the output buffer (0 on failure). +/// 4. Host reads the output, then calls `free(ptr, len)` on each buffer. +/// +/// See ../src/index.ts for the worker side and ../doc/memory.md for the model. +const std = @import("std"); +const resolve = @import("resolve.zig"); + +const allocator = std.heap.wasm_allocator; + +export fn alloc(len: u32) ?[*]u8 { + const buf = allocator.alloc(u8, len) catch return null; + return buf.ptr; +} + +export fn free(ptr: [*]u8, len: u32) void { + allocator.free(ptr[0..len]); +} + +/// Parse `spec` and write the docs.rs rustdoc JSON URL into the output buffer. +/// `target_len == 0` means "use the default host target". +export fn resolve_url( + spec_ptr: [*]const u8, + spec_len: u32, + target_ptr: [*]const u8, + target_len: u32, + out_ptr: [*]u8, + out_cap: u32, +) u32 { + const target: ?[]const u8 = if (target_len == 0) null else target_ptr[0..target_len]; + return resolve.resolveUrl( + allocator, + spec_ptr[0..spec_len], + target, + out_ptr[0..out_cap], + ); +} diff --git a/zig/package.json b/zig/package.json new file mode 100644 index 0000000..e33cf24 --- /dev/null +++ b/zig/package.json @@ -0,0 +1,18 @@ +{ + "name": "md-docrs-zig", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build:wasm": "cd lib && zig build && cp zig-out/bin/md-docrs.wasm ../src/md_docrs.wasm", + "build:cli": "cd lib && zig build cli", + "dev": "npm run build:wasm && wrangler dev", + "deploy": "npm run build:wasm && wrangler deploy", + "test:zig": "cd lib && zig build test", + "typecheck": "wrangler types && tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.7.0", + "wrangler": "latest" + } +} diff --git a/zig/src/index.ts b/zig/src/index.ts new file mode 100644 index 0000000..941a83b --- /dev/null +++ b/zig/src/index.ts @@ -0,0 +1,93 @@ +import mdDocrsWasm from "./md_docrs.wasm"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +interface WasmExports { + memory: WebAssembly.Memory; + alloc: (len: number) => number; + free: (ptr: number, len: number) => void; + resolve_url: ( + specPtr: number, + specLen: number, + targetPtr: number, + targetLen: number, + outPtr: number, + outCap: number, + ) => number; +} + +const OUT_CAP = 512; + +// Wrangler compiles the imported .wasm to a WebAssembly.Module at build time. +// Each request instantiates a fresh module: the Zig wasm_allocator has no +// global state we need to reset, but allocator arenas and Zig globals are +// cheap to recreate and it keeps requests isolated. +function resolveUrl(spec: string, target: string | null): string { + const instance = new WebAssembly.Instance(mdDocrsWasm); + const wasm = instance.exports as unknown as WasmExports; + + const specBytes = encoder.encode(spec); + const targetBytes = target ? encoder.encode(target) : new Uint8Array(0); + + const specPtr = wasm.alloc(specBytes.length); + const targetPtr = targetBytes.length ? wasm.alloc(targetBytes.length) : 0; + const outPtr = wasm.alloc(OUT_CAP); + if (specPtr === 0 || outPtr === 0 || (targetBytes.length && targetPtr === 0)) { + throw new Error("WASM alloc failed"); + } + + try { + // Views must be created *after* every alloc: WASM memory growth detaches + // any existing Uint8Array onto the old buffer. See ../doc-memory notes. + new Uint8Array(wasm.memory.buffer, specPtr, specBytes.length).set(specBytes); + if (targetBytes.length) { + new Uint8Array(wasm.memory.buffer, targetPtr, targetBytes.length).set(targetBytes); + } + + const n = wasm.resolve_url( + specPtr, + specBytes.length, + targetPtr, + targetBytes.length, + outPtr, + OUT_CAP, + ); + if (n === 0) throw new Error("invalid spec or output buffer too small"); + + return decoder.decode(new Uint8Array(wasm.memory.buffer, outPtr, n)); + } finally { + wasm.free(specPtr, specBytes.length); + if (targetBytes.length) wasm.free(targetPtr, targetBytes.length); + wasm.free(outPtr, OUT_CAP); + } +} + +export default { + async fetch(request: Request): Promise { + const url = new URL(request.url); + + // Spec can come from ?spec=... or the path (/serde::de::Deserialize). + const spec = url.searchParams.get("spec") ?? decodeURIComponent(url.pathname.replace(/^\//, "")); + if (!spec) { + return new Response( + "usage: GET /[?target=]\n" + + "example: /tokio@1.52.1::sync::Mutex?target=x86_64-unknown-linux-gnu\n", + { status: 400, headers: { "Content-Type": "text/plain; charset=utf-8" } }, + ); + } + + const target = url.searchParams.get("target"); + try { + const docsUrl = resolveUrl(spec, target); + return new Response(docsUrl + "\n", { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); + } catch (err) { + return new Response(`error: ${(err as Error).message}\n`, { + status: 400, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); + } + }, +}; diff --git a/zig/src/md_docrs.wasm.d.ts b/zig/src/md_docrs.wasm.d.ts new file mode 100644 index 0000000..481baad --- /dev/null +++ b/zig/src/md_docrs.wasm.d.ts @@ -0,0 +1,3 @@ +/** Zig-compiled WASM module — wrangler bundles this as a WebAssembly.Module. */ +declare const module: WebAssembly.Module; +export default module; diff --git a/zig/tsconfig.json b/zig/tsconfig.json new file mode 100644 index 0000000..63357d6 --- /dev/null +++ b/zig/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["./worker-configuration.d.ts"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/zig/wrangler.jsonc b/zig/wrangler.jsonc new file mode 100644 index 0000000..0ebde34 --- /dev/null +++ b/zig/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "md-docrs-zig", + "main": "src/index.ts", + "compatibility_date": "2026-04-20", + "rules": [{ "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }], + "observability": { + "logs": { "enabled": true } + } +} From 43f555afaa6690ec55d639741c20cd2ec84233ae Mon Sep 17 00:00:00 2001 From: Thomas Aubry Date: Mon, 20 Apr 2026 22:59:45 +0200 Subject: [PATCH 3/7] Add Rust wasm32 build mirroring the Zig WASM ABI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns the root crate into a workspace and feature-gates the HTTP / server / CLI surface (reqwest, zstd, tokio, axum, tower-http, tracing, clap) behind the `http`, `server`, and `cli` features so the pure pipeline — spec, resolve, render, cache — now compiles for `wasm32-unknown-unknown`. Adds a new `rust-wasm` workspace member exposing the exact same C ABI as `zig/lib/wasm.zig`: alloc(len) -> *u8 free(ptr, len) resolve_url(spec, target, out, out_cap) -> u32 render_markdown(json, spec, target, *len_out) -> *u8 (behind `render`) The no_mangle exports are gated to `target_arch = "wasm32"` so host tests don't shadow libc's free and cause infinite recursion during dealloc. Current sizes for `--profile wasm-release`: * resolve_url only (`--no-default-features`): 35,939 bytes * full pipeline (+render_markdown): 414,560 bytes The Zig worker at `zig/src/index.ts` works against either .wasm without changes. Porting render_markdown to Zig is the follow-up; that's where the JSON→Markdown comparison becomes apples-to-apples. https: //claude.ai/code/session_01Ei65b8fDC7sABmKnq1Wgne Co-authored-by: Claude --- Cargo.lock | 9 ++ Cargo.toml | 70 +++++++++--- README.md | 26 +++++ rust-wasm/Cargo.toml | 26 +++++ rust-wasm/README.md | 79 +++++++++++++ rust-wasm/src/lib.rs | 264 +++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 1 + src/lib.rs | 4 + zig/README.md | 27 ++++- 9 files changed, 488 insertions(+), 18 deletions(-) create mode 100644 rust-wasm/Cargo.toml create mode 100644 rust-wasm/README.md create mode 100644 rust-wasm/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ae9a759..ef3d367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -916,6 +916,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-docrs-wasm" +version = "0.1.0" +dependencies = [ + "md_docrs_proxy", + "rustdoc-types", + "serde_json", +] + [[package]] name = "md_docrs_proxy" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 64caafe..cf24743 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,8 @@ +[workspace] +members = [".", "rust-wasm"] +default-members = ["."] +resolver = "3" + [package] name = "md_docrs_proxy" version = "0.1.0" @@ -6,33 +11,68 @@ edition = "2024" [[bin]] name = "md-docrs" path = "src/main.rs" +required-features = ["cli"] [lib] name = "md_docrs_proxy" path = "src/lib.rs" +[features] +default = ["cli", "http", "server"] +# Pure pipeline (spec parse + resolve + render + in-memory cache) always compiles. +# `http` adds the docs.rs fetcher (reqwest + zstd + tokio + tracing). +http = ["dep:reqwest", "dep:zstd", "dep:tokio", "dep:tracing", "dep:bytes"] +# `server` layers the axum HTTP mirror on top of `http`. +server = ["http", "dep:axum", "dep:tower-http"] +# `cli` is only relevant for the `md-docrs` binary. +cli = [ + "http", + "server", + "dep:anyhow", + "dep:clap", + "dep:tokio", + "dep:tracing", + "dep:tracing-subscriber", +] +# Opt-in disk-backed cache via foyer. Not wasm-compatible. +hybrid-cache = ["dep:foyer", "dep:serde"] + [dependencies] -anyhow = "1" +# Always on — used by the pure pipeline (spec, resolve, render, cache). async-trait = "0.1" -axum = "0.8" -bytes = "1" -clap = { version = "4", features = ["derive"] } -foyer = { version = "0.22", optional = true, features = ["serde"] } lru = "0.17" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } rustdoc-types = "0.57" -serde = { version = "1", optional = true, features = ["derive", "rc"] } serde_json = "1" thiserror = "2" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] } -tower-http = { version = "0.6", features = ["trace"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -zstd = "0.13" -[features] -default = [] -hybrid-cache = ["dep:foyer", "dep:serde"] +# Optional — gated by features above. +anyhow = { version = "1", optional = true } +axum = { version = "0.8", optional = true } +bytes = { version = "1", optional = true } +clap = { version = "4", features = ["derive"], optional = true } +foyer = { version = "0.22", optional = true, features = ["serde"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"], optional = true } +serde = { version = "1", optional = true, features = ["derive", "rc"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"], optional = true } +tower-http = { version = "0.6", features = ["trace"], optional = true } +tracing = { version = "0.1", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } +zstd = { version = "0.13", optional = true } [profile.release] lto = "thin" + +# Squeeze the wasm artifact as tight as possible — this is the module we +# compare against Zig's ReleaseSmall build. +[profile.release.package.md-docrs-wasm] +opt-level = "z" +codegen-units = 1 +strip = true + +[profile.wasm-release] +inherits = "release" +opt-level = "z" +lto = "fat" +codegen-units = 1 +strip = true +panic = "abort" diff --git a/README.md b/README.md index 8b1957f..192df0f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,32 @@ Status codes: 404 item not found, 400 bad spec, 502 upstream/decode error. - v0 does not render trait impls, blanket impls, or source links. - Glob re-exports into external crates (e.g. `clap::Parser` from `clap_builder`) are not followed. +## WebAssembly builds + +Two same-ABI WASM modules live alongside the Rust library: + +- [`rust-wasm/`](rust-wasm/README.md) — `wasm32-unknown-unknown` build of + the pure pipeline (spec parse + resolve + render). Exports `alloc`, + `free`, `resolve_url`, and optionally `render_markdown`. +- [`zig/`](zig/README.md) — Zig 0.16 port of the same surface (`resolve_url` + parity today; `render_markdown` is a follow-up). Ships a Cloudflare Worker + wrapper that can load either artifact unchanged. + +Build the Rust wasm: + +```sh +# Minimal (resolve_url only — matches current Zig surface). +cargo build --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm --no-default-features +# Full (adds render_markdown, brings in serde_json + rustdoc-types). +cargo build --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm +``` + +The root crate's HTTP / server / CLI bits are gated behind `http`, `server`, +and `cli` features (all on by default), so the pure pipeline compiles for +`wasm32` without reqwest/tokio/axum/zstd. + ## Logging ```sh diff --git a/rust-wasm/Cargo.toml b/rust-wasm/Cargo.toml new file mode 100644 index 0000000..3287576 --- /dev/null +++ b/rust-wasm/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "md-docrs-wasm" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +# cdylib for the actual wasm artifact, rlib so `cargo test -p md-docrs-wasm` +# can exercise the exported functions on the host. +crate-type = ["cdylib", "rlib"] + +[features] +# Default: both surfaces. +# - `resolve_url` alone is the minimal parity build vs Zig (spec parse + URL). +# - `render` pulls in serde_json + rustdoc-types for the full JSON→Markdown +# pipeline. This roughly triples the .wasm size; toggle off for +# size-parity size comparisons. +default = ["render"] +render = ["dep:serde_json", "dep:rustdoc-types"] + +[dependencies] +# Pulls in only the pure pipeline (spec / resolve / render / cache). +md_docrs_proxy = { path = "..", default-features = false } +rustdoc-types = { version = "0.57", optional = true } +serde_json = { version = "1", optional = true } + diff --git a/rust-wasm/README.md b/rust-wasm/README.md new file mode 100644 index 0000000..91c5c2e --- /dev/null +++ b/rust-wasm/README.md @@ -0,0 +1,79 @@ +# md-docrs-wasm + +`wasm32-unknown-unknown` build of the `md_docrs_proxy` pure pipeline, exposing +the **exact same C ABI** as the Zig build (`zig/lib/wasm.zig`). Lets us drop +either `.wasm` into the same host and compare size and per-request latency +without any host-side code changes. + +## Exports + +| Symbol | Signature | Notes | +| --- | --- | --- | +| `alloc` | `(len: u32) -> *u8` | Backed by Rust's global allocator. Returns null on OOM or `len == 0`. | +| `free` | `(ptr: *u8, len: u32)` | Length must match the allocation. | +| `resolve_url` | `(spec_ptr, spec_len, target_ptr, target_len, out_ptr, out_cap) -> u32` | Same semantics as the Zig export. 0 on error. | +| `render_markdown` | `(json_ptr, json_len, spec_ptr, spec_len, target_ptr, target_len, len_out: *u32) -> *u8` | Takes already-decoded rustdoc JSON, returns a fresh allocation containing Markdown. Caller frees. Null on error. Only present in the `render` feature build. | + +## Building + +```sh +# Minimal parity build — matches the Zig wasm surface (resolve_url only). +cargo build --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm --no-default-features + +# Full pipeline — adds render_markdown (serde_json + rustdoc-types). +cargo build --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm +``` + +Artifact lives at `target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm`. + +## Size snapshot + +Measured on Rust 1.94 / Zig 0.16 with no post-build shrinking (no `wasm-opt`). + +| Build | Bytes | +| --- | ---: | +| Zig 0.16 — `ReleaseSmall` + `strip`, exports `resolve_url` | *TBD (run `cd zig/lib && zig build && wc -c zig-out/bin/md-docrs.wasm`)* | +| Rust `wasm-release` — `resolve_url` only (`--no-default-features`) | **35,939** | +| Rust `wasm-release` — `resolve_url` + `render_markdown` | **414,560** | + +The ~10x jump for `render_markdown` is serde_json + `rustdoc-types` deserialise +impls. Expected; that's the cost of JSON→AST→Markdown. + +## Feature gates + +- `render` (default) — pulls `serde_json` + `rustdoc-types` and exposes + `render_markdown`. Turn off for the minimal size-parity build. + +## Tests + +Host tests run through the same internal functions as the WASM exports +(the `no_mangle` attribute is gated to `target_arch = "wasm32"` so the test +binary doesn't shadow libc's `free`): + +```sh +cargo test -p md-docrs-wasm +``` + +## Comparing with Zig + +Both modules share this memory protocol: + +1. Host calls `alloc(n)` to reserve input / output buffers in linear memory. +2. Host writes input bytes into those buffers via a fresh `Uint8Array(memory.buffer, ptr, len)`. +3. Host calls `resolve_url(...)` (or `render_markdown(...)`). +4. Host reads the output, then calls `free(ptr, len)` on each buffer. + +Because the ABI matches byte-for-byte, the Worker at `zig/src/index.ts` +works as-is against either module — just point the `.wasm` import at the +Rust artifact. + +## What's next + +- Port `render_markdown` to Zig. That's where the real interesting size / + speed comparison happens — today the Zig wasm doesn't carry serde_json + or the rustdoc types. +- Benchmark instantiation + per-call latency side-by-side in a Worker + (e.g. hyperfine-style loop from a test harness, or wrangler dev + `wrk`). +- Optional: run `wasm-opt -Oz` on both artifacts for a true "shipped" size. diff --git a/rust-wasm/src/lib.rs b/rust-wasm/src/lib.rs new file mode 100644 index 0000000..5a9bb6e --- /dev/null +++ b/rust-wasm/src/lib.rs @@ -0,0 +1,264 @@ +//! WASM-friendly C ABI over the `md_docrs_proxy` pure pipeline. +//! +//! Exposes the same `alloc` / `free` / `resolve_url` trio as the Zig build +//! (`zig/lib/wasm.zig`) so both modules are drop-in interchangeable behind +//! the same Cloudflare Worker. Adds `render_markdown`, which takes an already +//! decoded rustdoc JSON blob plus a spec and returns rendered Markdown — the +//! piece the Zig side will eventually mirror for the full-pipeline benchmark. + +use md_docrs_proxy::ItemSpec; +#[cfg(feature = "render")] +use md_docrs_proxy::{render, resolve}; +#[cfg(feature = "render")] +use rustdoc_types::Crate; +use std::alloc::{Layout, alloc as rust_alloc, dealloc}; +use std::ptr; +use std::slice; + +/// rustdoc JSON format version this build targets. Kept in sync with the Zig +/// build and the `rustdoc-types` dependency. +const FORMAT_VERSION: u32 = 57; +const DOCS_RS_BASE: &str = "https://docs.rs"; + +fn layout_for(len: usize) -> Option { + if len == 0 { None } else { Layout::array::(len).ok() } +} + +/// Allocate `len` bytes inside the WASM linear memory. Returns null on failure +/// or when `len == 0`. Caller must free with `free(ptr, len)`. +#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))] +pub extern "C" fn alloc(len: u32) -> *mut u8 { + let Some(layout) = layout_for(len as usize) else { + return ptr::null_mut(); + }; + // SAFETY: layout has non-zero size (checked above). + unsafe { rust_alloc(layout) } +} + +/// Free memory previously returned by `alloc`. `len` must match the allocation. +/// +/// # Safety +/// `ptr` must be a pointer returned by `alloc` with the exact same `len`. +#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))] +pub unsafe extern "C" fn free(ptr: *mut u8, len: u32) { + if ptr.is_null() { + return; + } + let Some(layout) = layout_for(len as usize) else { + return; + }; + unsafe { dealloc(ptr, layout) }; +} + +/// Parse `spec` and write the docs.rs rustdoc JSON URL into `out_ptr`. +/// `target_len == 0` means "use the default host target". +/// Returns bytes written, or 0 on any failure. +/// +/// # Safety +/// All four (ptr, len) pairs must describe valid readable / writable slices +/// inside WASM linear memory. +#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))] +pub unsafe extern "C" fn resolve_url( + spec_ptr: *const u8, + spec_len: u32, + target_ptr: *const u8, + target_len: u32, + out_ptr: *mut u8, + out_cap: u32, +) -> u32 { + let spec_bytes = unsafe { slice::from_raw_parts(spec_ptr, spec_len as usize) }; + let Ok(spec_str) = std::str::from_utf8(spec_bytes) else { + return 0; + }; + let Ok(mut spec) = ItemSpec::parse(spec_str) else { + return 0; + }; + + if target_len > 0 { + let t = unsafe { slice::from_raw_parts(target_ptr, target_len as usize) }; + let Ok(t_str) = std::str::from_utf8(t) else { + return 0; + }; + spec = spec.with_target(Some(t_str.to_string())); + } + + let url = match spec.target.as_deref() { + Some(t) => format!( + "{DOCS_RS_BASE}/crate/{}/{}/{}/json/{FORMAT_VERSION}.zst", + spec.crate_name, spec.version, t + ), + None => format!( + "{DOCS_RS_BASE}/crate/{}/{}/json/{FORMAT_VERSION}.zst", + spec.crate_name, spec.version + ), + }; + + let bytes = url.as_bytes(); + if bytes.len() > out_cap as usize { + return 0; + } + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr, bytes.len()); + } + bytes.len() as u32 +} + +/// Render rustdoc JSON to Markdown. +/// +/// The caller owns `json_ptr` / `spec_ptr` / `target_ptr`. On success this +/// returns a pointer to a fresh buffer (allocated with `alloc`) containing +/// the Markdown, and writes the byte length to `*len_out`. The caller is +/// responsible for `free(ptr, *len_out)`. +/// +/// Returns null on any error (invalid spec, JSON parse failure, resolve +/// miss, alloc failure). `*len_out` is only meaningful when the return +/// value is non-null. +/// +/// Output length is not known in advance (varies with the item's doc size) +/// so we allocate here rather than asking the caller to guess a bound. +/// +/// # Safety +/// All input (ptr, len) pairs must describe valid readable slices. `len_out` +/// must be a writable `u32`. +#[cfg(feature = "render")] +#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))] +pub unsafe extern "C" fn render_markdown( + json_ptr: *const u8, + json_len: u32, + spec_ptr: *const u8, + spec_len: u32, + target_ptr: *const u8, + target_len: u32, + len_out: *mut u32, +) -> *mut u8 { + let json = unsafe { slice::from_raw_parts(json_ptr, json_len as usize) }; + let spec_bytes = unsafe { slice::from_raw_parts(spec_ptr, spec_len as usize) }; + + let Ok(spec_str) = std::str::from_utf8(spec_bytes) else { + return ptr::null_mut(); + }; + let Ok(mut spec) = ItemSpec::parse(spec_str) else { + return ptr::null_mut(); + }; + if target_len > 0 { + let t = unsafe { slice::from_raw_parts(target_ptr, target_len as usize) }; + let Ok(t_str) = std::str::from_utf8(t) else { + return ptr::null_mut(); + }; + spec = spec.with_target(Some(t_str.to_string())); + } + + let Ok(krate) = serde_json::from_slice::(json) else { + return ptr::null_mut(); + }; + let Ok(resolved) = resolve::resolve(&krate, &spec) else { + return ptr::null_mut(); + }; + let md = render::render(&krate, &resolved, &spec); + + let bytes = md.as_bytes(); + let Some(layout) = layout_for(bytes.len()) else { + return ptr::null_mut(); + }; + // SAFETY: non-zero layout. + let out = unsafe { rust_alloc(layout) }; + if out.is_null() { + return ptr::null_mut(); + } + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), out, bytes.len()); + *len_out = bytes.len() as u32; + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_url_basic() { + let spec = b"serde"; + let mut out = [0u8; 128]; + let n = unsafe { + resolve_url( + spec.as_ptr(), + spec.len() as u32, + ptr::null(), + 0, + out.as_mut_ptr(), + out.len() as u32, + ) + }; + assert_eq!( + std::str::from_utf8(&out[..n as usize]).unwrap(), + "https://docs.rs/crate/serde/latest/json/57.zst", + ); + } + + #[test] + fn resolve_url_with_target_and_pinned_version() { + let spec = b"tokio@1.52.1::sync::Mutex"; + let target = b"x86_64-unknown-linux-gnu"; + let mut out = [0u8; 256]; + let n = unsafe { + resolve_url( + spec.as_ptr(), + spec.len() as u32, + target.as_ptr(), + target.len() as u32, + out.as_mut_ptr(), + out.len() as u32, + ) + }; + assert_eq!( + std::str::from_utf8(&out[..n as usize]).unwrap(), + "https://docs.rs/crate/tokio/1.52.1/x86_64-unknown-linux-gnu/json/57.zst", + ); + } + + #[test] + fn resolve_url_bad_spec_returns_zero() { + let spec = b"1bad"; + let mut out = [0u8; 128]; + let n = unsafe { + resolve_url( + spec.as_ptr(), + spec.len() as u32, + ptr::null(), + 0, + out.as_mut_ptr(), + out.len() as u32, + ) + }; + assert_eq!(n, 0); + } + + #[test] + fn resolve_url_output_too_small() { + let spec = b"serde"; + let mut out = [0u8; 8]; + let n = unsafe { + resolve_url( + spec.as_ptr(), + spec.len() as u32, + ptr::null(), + 0, + out.as_mut_ptr(), + out.len() as u32, + ) + }; + assert_eq!(n, 0); + } + + #[test] + fn alloc_and_free_roundtrip() { + let ptr = alloc(64); + assert!(!ptr.is_null()); + unsafe { + *ptr = 42; + assert_eq!(*ptr, 42); + free(ptr, 64); + } + } +} diff --git a/src/error.rs b/src/error.rs index 72bf3ee..0d8733b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,6 +22,7 @@ pub enum Error { #[error("io error: {0}")] Io(#[from] std::io::Error), + #[cfg(feature = "http")] #[error("http error: {0}")] Http(#[from] reqwest::Error), } diff --git a/src/lib.rs b/src/lib.rs index 8ae578b..c385c4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod cache; pub mod error; +#[cfg(feature = "http")] pub mod fetch; pub mod render; pub mod resolve; @@ -10,6 +11,7 @@ pub mod spec; pub use error::{Error, Result}; pub use spec::ItemSpec; +#[cfg(feature = "http")] use std::sync::Arc; /// High-level entry point: take a parsed `ItemSpec`, return rendered Markdown. @@ -21,6 +23,7 @@ use std::sync::Arc; /// # Errors /// Forwards errors from `Fetcher::fetch` (network / docs.rs / decode failures) /// and `resolve::resolve` (`Error::NotFound` when the path does not match). +#[cfg(feature = "http")] pub async fn render_spec( spec: &ItemSpec, fetcher: &fetch::Fetcher, @@ -31,6 +34,7 @@ pub async fn render_spec( Ok(render::render(&krate, &resolved, spec)) } +#[cfg(feature = "http")] async fn load_crate( spec: &ItemSpec, fetcher: &fetch::Fetcher, diff --git a/zig/README.md b/zig/README.md index fdc757a..3881daa 100644 --- a/zig/README.md +++ b/zig/README.md @@ -81,8 +81,29 @@ Memory protocol notes in the zigflare [`doc/memory.md`](https://github.com/mattz ## Comparing with Rust WASM -Next step is adding a `wasm32-unknown-unknown` target to the Rust crate that exposes the same `resolve_url` surface. We can then compare: +The Rust equivalent lives at [`../rust-wasm/`](../rust-wasm/README.md). It exports +the same `alloc` / `free` / `resolve_url` symbols with byte-for-byte identical +signatures, so the Worker at `src/index.ts` can swap between the two by changing +a single import path. -- `.wasm` size (Rust with `panic=abort` + LTO + `wasm-opt` vs. Zig `ReleaseSmall` + `strip`). -- Instantiation + call latency in a Worker. +```sh +# Minimal parity build — matches this Zig wasm surface 1:1. +cargo build --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm --no-default-features +cp ../target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm \ + src/md_docrs.wasm # drop-in replacement for the Zig artifact + +# Full pipeline build — also exports `render_markdown` (JSON → Markdown). +cargo build --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm +``` + +What we're comparing: + +- `.wasm` size (Zig `ReleaseSmall` + `strip` vs. Rust `opt-level=z` + fat LTO + `strip`). +- Instantiation + per-call latency in a Worker. - Cold-start cost (wrangler measures this). + +The Rust README has the current byte counts. Porting `render_markdown` to +Zig is the interesting follow-up — that's where serde_json / rustdoc-types +vs. `std.json` + hand-written types becomes a real apples-to-apples test. From 854c6630087abaf9b6a327e3bf77583be4d7e41a Mon Sep 17 00:00:00 2001 From: Thomas Aubry Date: Mon, 20 Apr 2026 23:04:10 +0200 Subject: [PATCH 4/7] Fix zig build.zig.zon fingerprint for Zig 0.16 The previous fingerprint (0xb3f2a91c4d6e7058) was rejected by Zig 0.16's stricter validator. Replace it with the value Zig derives from the package name (.md_docrs_zig) so 'zig build' succeeds out of the box. Co-authored-by: Claude --- zig/lib/build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zig/lib/build.zig.zon b/zig/lib/build.zig.zon index 6b53609..f9dddab 100644 --- a/zig/lib/build.zig.zon +++ b/zig/lib/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .md_docrs_zig, .version = "0.1.0", - .fingerprint = 0xb3f2a91c4d6e7058, + .fingerprint = 0x8259478368ccd9fe, .minimum_zig_version = "0.16.0", .dependencies = .{}, .paths = .{ From 84f09a53dfb07c1f76a1c6fd986d63406fa0b2de Mon Sep 17 00:00:00 2001 From: Thomas Aubry Date: Tue, 21 Apr 2026 09:43:53 +0200 Subject: [PATCH 5/7] Port native Zig CLI to Zig 0.16 std.process.Init entry point Zig 0.16 removed std.heap.GeneralPurposeAllocator and std.process.argsAlloc in favour of main(init: std.process.Init) with an arena + Io handed in by the startup shim. cli.zig now takes that parameter, returns an exit code (!u8), and uses the new Io.File.Writer for stdout/stderr, so 'zig build cli' and 'zig build run -- ...' both succeed against the shipped 0.16.0 stdlib. Expand the native-CLI section of zig/README.md with concrete spec examples and document the exit-code convention so the binary is actually usable, not just buildable. Co-authored-by: Claude --- zig/README.md | 66 ++++++++++++++++++++++++++++++++++++++++--------- zig/lib/cli.zig | 34 ++++++++++++------------- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/zig/README.md b/zig/README.md index 3881daa..a49d651 100644 --- a/zig/README.md +++ b/zig/README.md @@ -30,22 +30,58 @@ zig/ ## Build +Everything runs from `zig/lib/`. `zig build` produces only the WASM artifact +by default — the CLI and tests are explicit steps so `npm run build:wasm` +stays focused. + ```sh -# WASM only (default target) -cd zig/lib && zig build +cd zig/lib + +# WASM (default step). +zig build # -> zig-out/bin/md-docrs.wasm -# Native CLI -cd zig/lib && zig build cli -./zig-out/bin/md-docrs-zig serde::de::Deserialize +# Native CLI. +zig build cli +# -> zig-out/bin/md-docrs-zig + +# Unit tests (spec / url / resolve). +zig build test +``` + +## Native CLI + +`md-docrs-zig` wraps the same `resolve.resolveUrl` that the WASM build +exports, so it's the fastest way to sanity-check a spec without spinning +up the Worker. + +```sh +cd zig/lib +zig build cli + +# Run directly. +./zig-out/bin/md-docrs-zig serde +# https://docs.rs/crate/serde/latest/json/57.zst + +./zig-out/bin/md-docrs-zig 'tokio@1.52.1::sync::Mutex' +# https://docs.rs/crate/tokio/1.52.1/json/57.zst + +./zig-out/bin/md-docrs-zig 'anyhow::Error' --target x86_64-unknown-linux-gnu +# https://docs.rs/crate/anyhow/latest/x86_64-unknown-linux-gnu/json/57.zst -# Tests (native, pull in spec.zig + url.zig + resolve.zig tests) -cd zig/lib && zig build test +./zig-out/bin/md-docrs-zig --help -# Run CLI through the build system -cd zig/lib && zig build run -- tokio@1.52.1::sync::Mutex --target x86_64-unknown-linux-gnu +# Or run through the build system (rebuilds if needed, forwards args after --). +zig build run -- 'tokio@1.52.1::sync::Mutex' --target x86_64-unknown-linux-gnu ``` +Exit codes: + +| Code | Meaning | +| --- | --- | +| 0 | URL printed to stdout. | +| 2 | Bad spec, missing `--target` value, or unknown argument (usage on stderr). | + ## Worker ```sh @@ -104,6 +140,12 @@ What we're comparing: - Instantiation + per-call latency in a Worker. - Cold-start cost (wrangler measures this). -The Rust README has the current byte counts. Porting `render_markdown` to -Zig is the interesting follow-up — that's where serde_json / rustdoc-types -vs. `std.json` + hand-written types becomes a real apples-to-apples test. +For a host-neutral comparison that doesn't involve wrangler, use the +[`wasm/`](../wasm/README.md) harness at the repo root. It builds both +modules, runs the exact same specs through each inside embedded wasmtime +(optionally wasmer), and reports byte size, output parity, and median / p95 +per-call latency in a single table. + +Porting `render_markdown` to Zig is the interesting follow-up — that's +where serde_json / rustdoc-types vs. `std.json` + hand-written types +becomes a real apples-to-apples test. diff --git a/zig/lib/cli.zig b/zig/lib/cli.zig index 7c654f1..b664f29 100644 --- a/zig/lib/cli.zig +++ b/zig/lib/cli.zig @@ -1,6 +1,7 @@ /// Native CLI entry point. Thin wrapper over `resolve.resolveUrl` so the exact -/// same code path is exercised by `zig build run` and the WASM worker. +/// same code path is exercised by `zig build cli -- ` and the WASM worker. const std = @import("std"); +const Io = std.Io; const resolve = @import("resolve.zig"); const usage = @@ -15,21 +16,19 @@ const usage = \\ ; -pub fn main() !void { - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); +pub fn main(init: std.process.Init) !u8 { + const arena = init.arena.allocator(); + const io = init.io; - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + const args = try init.minimal.args.toSlice(arena); var stdout_buf: [4096]u8 = undefined; - var stdout_file = std.fs.File.stdout().writer(&stdout_buf); - const stdout = &stdout_file.interface; + var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buf); + const stdout = &stdout_file_writer.interface; var stderr_buf: [4096]u8 = undefined; - var stderr_file = std.fs.File.stderr().writer(&stderr_buf); - const stderr = &stderr_file.interface; + var stderr_file_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buf); + const stderr = &stderr_file_writer.interface; var spec_arg: ?[]const u8 = null; var target: ?[]const u8 = null; @@ -42,36 +41,37 @@ pub fn main() !void { if (i >= args.len) { try stderr.writeAll("error: --target requires a value\n"); try stderr.flush(); - std.process.exit(2); + return 2; } target = args[i]; } else if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "--help")) { try stdout.writeAll(usage); try stdout.flush(); - return; + return 0; } else if (spec_arg == null) { spec_arg = a; } else { try stderr.print("error: unexpected argument: {s}\n", .{a}); try stderr.flush(); - std.process.exit(2); + return 2; } } const raw = spec_arg orelse { try stderr.writeAll(usage); try stderr.flush(); - std.process.exit(2); + return 2; }; var buf: [512]u8 = undefined; - const n = resolve.resolveUrl(allocator, raw, target, &buf); + const n = resolve.resolveUrl(arena, raw, target, &buf); if (n == 0) { try stderr.print("error: could not resolve URL for '{s}'\n", .{raw}); try stderr.flush(); - std.process.exit(2); + return 2; } try stdout.print("{s}\n", .{buf[0..n]}); try stdout.flush(); + return 0; } From 789a0c456bb1004fdc700ad7fedd3da5faf9a700 Mon Sep 17 00:00:00 2001 From: Thomas Aubry Date: Tue, 21 Apr 2026 09:49:31 +0200 Subject: [PATCH 6/7] Add wasm/ harness comparing Zig and Rust wasm under wasmtime/wasmer Adds a new workspace member at wasm/ (md-docrs-wasm-compare) that loads each .wasm artifact inside an embedded wasmtime host, drives the shared resolve_url ABI through a fixed spec list, and reports byte size, output parity, and median / p95 per-call latency in one table. An optional 'wasmer' cargo feature swaps the runtime to wasmer under --runtime wasmer so ABI cost and JIT cost can be separated. wasm/build.sh builds zig (ReleaseSmall) + rust-minimal (no-default-features) + rust-full (render_markdown) and stages them as zig.wasm / rust-minimal.wasm / rust-full.wasm under wasm/artifacts/, which the harness picks up by default via CARGO_MANIFEST_DIR. Missing artifacts are skipped rather than erroring so partial runs still print something useful. Parity confirmed against all three modules on the default spec list (serde, tokio@1.52.1::sync::Mutex, anyhow::Error with target override, rustdoc-types@0.57::Crate): byte-identical output from every artifact under both runtimes. Co-authored-by: Claude --- Cargo.lock | 4249 ++++++++++++++++++++++++++++++++++++---------- Cargo.toml | 2 +- README.md | 9 + wasm/.gitignore | 1 + wasm/Cargo.toml | 17 + wasm/README.md | 95 ++ wasm/build.sh | 33 + wasm/src/main.rs | 386 +++++ 8 files changed, 3860 insertions(+), 932 deletions(-) create mode 100644 wasm/.gitignore create mode 100644 wasm/Cargo.toml create mode 100644 wasm/README.md create mode 100755 wasm/build.sh create mode 100644 wasm/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ef3d367..372d120 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli 0.32.3", +] + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli 0.33.0", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -73,6 +97,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "async-trait" version = "0.1.89" @@ -148,6 +178,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line 0.25.1", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.37.3", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.22.1" @@ -163,17 +208,87 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "bytes" @@ -181,6 +296,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "cc" version = "1.2.60" @@ -193,6 +314,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -205,6 +335,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.6.1" @@ -245,18 +386,51 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "cmsketch" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7ee2cfacbd29706479902b06d75ad8f1362900836aa32799eabc7e004bfd854" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core_affinity" version = "0.8.3" @@ -269,1611 +443,3658 @@ dependencies = [ ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "corosensei" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", ] [[package]] -name = "either" -version = "1.15.0" +name = "cpp_demangle" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] [[package]] -name = "equivalent" -version = "1.0.2" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] -name = "errno" -version = "0.3.14" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", - "windows-sys 0.61.2", ] [[package]] -name = "fastant" -version = "0.1.11" +name = "cranelift-assembler-x64" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e825441bfb2d831c47c97d05821552db8832479f44c571b97fededbf0099c07" +checksum = "4b242b4c3675139f52f0b55624fb92571551a344305c5998f55ad20fa527bc55" dependencies = [ - "small_ctor", - "web-time", + "cranelift-assembler-x64-meta 0.129.2", ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "cranelift-assembler-x64" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "6edb5bdd1af46714e3224a017fabbbd57f70df4e840eb5ad6a7429dc456119d6" +dependencies = [ + "cranelift-assembler-x64-meta 0.131.0", +] [[package]] -name = "foldhash" -version = "0.2.0" +name = "cranelift-assembler-x64-meta" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "499715f19799219f32641b14f2a162f91e50bc1b61c2d2184c2be971716f5c56" +dependencies = [ + "cranelift-srcgen 0.129.2", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "cranelift-assembler-x64-meta" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "a819599186e1b1a1f88d464e06045696afc7aa3e0cc018aa0b2999cb63d1d088" dependencies = [ - "percent-encoding", + "cranelift-srcgen 0.131.0", ] [[package]] -name = "foyer" -version = "0.22.3" +name = "cranelift-bforest" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0abc0b87814989efa711f9becd9f26969820e2d3905db27d10969c4bd45890" +checksum = "9a92d78cc3f087d7e7073828f08d98c7074a3a062b6b29a1b7783ce74305685e" dependencies = [ - "anyhow", - "equivalent", - "foyer-common", - "foyer-memory", - "foyer-storage", - "foyer-tokio", - "futures-util", - "mea", - "mixtrics", - "pin-project", - "serde", - "tracing", + "cranelift-entity 0.129.1", ] [[package]] -name = "foyer-common" -version = "0.22.3" +name = "cranelift-bforest" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3db80d5dece93adb7ad709c84578794724a9cba342a7e566c3551c7ec626789" +checksum = "36e2c152d488e03c87b913bc2ed3414416eb1e0d66d61b49af60bf456a9665c7" dependencies = [ - "anyhow", - "bincode", - "bytes", - "cfg-if", - "foyer-tokio", - "mixtrics", - "parking_lot", - "pin-project", - "serde", - "twox-hash", + "cranelift-entity 0.131.0", + "wasmtime-internal-core 44.0.0", ] [[package]] -name = "foyer-intrusive-collections" -version = "0.10.0-dev" +name = "cranelift-bitset" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4fee46bea69e0596130e3210e65d3424e0ac1e6df3bde6636304bdf1ca4a3b" +checksum = "edcc73d756f2e0d7eda6144fe64a2bc69c624de893cb1be51f1442aed77881d2" dependencies = [ - "memoffset", + "wasmtime-internal-core 42.0.1", ] [[package]] -name = "foyer-memory" -version = "0.22.3" +name = "cranelift-bitset" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db907f40a527ca2aa2f40a5f68b32ea58aa70f050cd233518e9ffd402cfba6ce" +checksum = "b6559d4fbc253d1396e1f6beeae57fa88a244f02aaf0cde2a735afd3492d9b2e" dependencies = [ - "anyhow", - "bitflags", - "cmsketch", - "equivalent", - "foyer-common", - "foyer-intrusive-collections", - "foyer-tokio", - "futures-util", - "hashbrown 0.16.1", - "itertools", - "mea", - "mixtrics", - "parking_lot", - "paste", - "pin-project", "serde", - "tracing", + "serde_derive", + "wasmtime-internal-core 44.0.0", ] [[package]] -name = "foyer-storage" -version = "0.22.3" +name = "cranelift-codegen" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1983f1db3d0710e9c9d5fc116d9202dccd41a2d1e032572224f1aff5520aa958" +checksum = "683d94c2cd0d73b41369b88da1129589bc3a2d99cf49979af1d14751f35b7a1b" dependencies = [ - "allocator-api2", - "anyhow", - "bytes", - "core_affinity", - "equivalent", - "fastant", - "foyer-common", - "foyer-memory", - "foyer-tokio", - "fs4", - "futures-core", - "futures-util", - "hashbrown 0.16.1", - "io-uring", - "itertools", - "libc", - "lz4", - "mea", - "parking_lot", - "pin-project", - "rand", + "bumpalo", + "cranelift-assembler-x64 0.129.2", + "cranelift-bforest 0.129.1", + "cranelift-bitset 0.129.1", + "cranelift-codegen-meta 0.129.2", + "cranelift-codegen-shared 0.129.2", + "cranelift-control 0.129.2", + "cranelift-entity 0.129.1", + "cranelift-isle 0.129.1", + "gimli 0.33.0", + "hashbrown 0.15.5", + "libm", + "log", + "regalloc2 0.13.5", + "rustc-hash", "serde", - "tracing", - "twox-hash", - "zstd", + "smallvec", + "target-lexicon", + "wasmtime-internal-core 42.0.1", ] [[package]] -name = "foyer-tokio" -version = "0.22.3" +name = "cranelift-codegen" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6577b05a7ffad0db555aedf00bfe52af818220fc4c1c3a7a12520896fc38627" +checksum = "96d9315d98d6e0a64454d4c83be2ee0e8055c3f80c3b2d7bcad7079f281a06ff" dependencies = [ - "tokio", + "bumpalo", + "cranelift-assembler-x64 0.131.0", + "cranelift-bforest 0.131.0", + "cranelift-bitset 0.131.0", + "cranelift-codegen-meta 0.131.0", + "cranelift-codegen-shared 0.131.0", + "cranelift-control 0.131.0", + "cranelift-entity 0.131.0", + "cranelift-isle 0.131.0", + "gimli 0.33.0", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2 0.15.1", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core 44.0.0", ] [[package]] -name = "fs4" -version = "0.13.1" +name = "cranelift-codegen-meta" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +checksum = "483b2c94a1b7f6fba0714387ba34ca56d114b2214a80be018acbb2ed40e09a1e" dependencies = [ - "rustix", - "windows-sys 0.59.0", + "cranelift-assembler-x64-meta 0.129.2", + "cranelift-codegen-shared 0.129.2", + "cranelift-srcgen 0.129.2", + "heck", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "cranelift-codegen-meta" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "d89c00a88081c55e3087c45bebc77e0cc973de2d7b44ef6a943c7122647b89f5" dependencies = [ - "futures-core", + "cranelift-assembler-x64-meta 0.131.0", + "cranelift-codegen-shared 0.131.0", + "cranelift-srcgen 0.131.0", + "heck", + "pulley-interpreter", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "cranelift-codegen-shared" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "c4aae718c336a52d90d4ebe9a2d8c3cf0906a4bee78f0e6867e777eebbe554fe" [[package]] -name = "futures-task" -version = "0.3.32" +name = "cranelift-codegen-shared" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "879f77c497a1eb6273482aa1ac3b23cb8563ff04edb39ed5dfcfd28c8deff8f5" [[package]] -name = "futures-util" -version = "0.3.32" +name = "cranelift-control" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a18e94519070dc56cddb71906a08cea6a28a1d7c58ed501b88f273fa6b45fa07" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "arbitrary", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "cranelift-control" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "498dc1f17a6910c88316d49c7176d8fa97cf10c30859c32a266040449317f963" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "arbitrary", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "cranelift-entity" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "59d8e72637246edd2cba337939850caa8b201f6315925ec4c156fdd089999699" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", + "cranelift-bitset 0.129.1", + "wasmtime-internal-core 42.0.1", ] [[package]] -name = "hashbrown" -version = "0.16.1" +name = "cranelift-entity" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "c2acba797f6a46042ce82aaf7680d0c3567fe2001e238db9df649fd104a2727f" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "cranelift-bitset 0.131.0", + "serde", + "serde_derive", + "wasmtime-internal-core 44.0.0", ] [[package]] -name = "hashbrown" -version = "0.17.0" +name = "cranelift-frontend" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "4c31db0085c3dfa131e739c3b26f9f9c84d69a9459627aac1ac4ef8355e3411b" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "cranelift-codegen 0.129.1", + "log", + "smallvec", + "target-lexicon", ] [[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" +name = "cranelift-frontend" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "4dca3df1d107d98d88f159ad1d5eaa2d5cdb678b3d5bcfadc6fc83d8ebb448ea" +dependencies = [ + "cranelift-codegen 0.131.0", + "log", + "smallvec", + "target-lexicon", +] [[package]] -name = "http" -version = "1.4.0" +name = "cranelift-isle" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] +checksum = "524d804c1ebd8c542e6f64e71aa36934cec17c5da4a9ae3799796220317f5d23" [[package]] -name = "http-body" -version = "1.0.1" +name = "cranelift-isle" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] +checksum = "f62dd18116d88bed649871feceda79dad7b59cc685ea8998c2b3e64d0e689602" [[package]] -name = "http-body-util" -version = "0.1.3" +name = "cranelift-native" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "f843b80360d7fdf61a6124642af7597f6d55724cf521210c34af8a1c66daca6e" dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", + "cranelift-codegen 0.131.0", + "libc", + "target-lexicon", ] [[package]] -name = "httparse" -version = "1.10.1" +name = "cranelift-srcgen" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +checksum = "4a1a001a9dc4557d9e2be324bc932621c0aa9bf33b74dfefa2338f0bf8913329" [[package]] -name = "httpdate" -version = "1.0.3" +name = "cranelift-srcgen" +version = "0.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "090ee5de58c6f17eb5e3a5ae8cf1695c7efea04ec4dd0ecba6a5b996c9bad7dc" [[package]] -name = "hyper" -version = "1.9.0" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", + "cfg-if", ] [[package]] -name = "hyper-rustls" -version = "0.27.9" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", + "crossbeam-utils", ] [[package]] -name = "hyper-util" -version = "0.1.20" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", + "crossbeam-epoch", + "crossbeam-utils", ] [[package]] -name = "icu_collections" -version = "2.2.0" +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", + "crossbeam-utils", ] [[package]] -name = "icu_locale_core" -version = "2.2.0" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "crossbeam-utils", ] [[package]] -name = "icu_normalizer" -version = "2.2.0" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "icu_normalizer_data" -version = "2.2.0" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] [[package]] -name = "icu_properties" -version = "2.2.0" +name = "crypto-common" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", + "hybrid-array", ] [[package]] -name = "icu_properties_data" -version = "2.2.0" +name = "darling" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] [[package]] -name = "icu_provider" -version = "2.2.0" +name = "darling_core" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "idna" -version = "1.1.0" +name = "darling_macro" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "darling_core", + "quote", + "syn", ] [[package]] -name = "idna_adapter" -version = "1.2.1" +name = "dashmap" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ - "icu_normalizer", - "icu_properties", + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] -name = "io-uring" -version = "0.7.12" +name = "debugid" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ - "bitflags", - "cfg-if", - "libc", + "uuid", ] [[package]] -name = "ipnet" -version = "2.12.0" +name = "derive_more" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] [[package]] -name = "iri-string" -version = "0.7.12" +name = "derive_more-impl" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "memchr", - "serde", + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] [[package]] -name = "itertools" -version = "0.14.0" +name = "digest" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ - "either", + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "directories-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] [[package]] -name = "jobserver" -version = "0.1.34" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ - "getrandom 0.3.4", "libc", + "redox_users", + "winapi", ] [[package]] -name = "js-sys" -version = "0.3.95" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "lazy_static" +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastant" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e825441bfb2d831c47c97d05821552db8832479f44c571b97fededbf0099c07" +dependencies = [ + "small_ctor", + "web-time", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "foyer" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0abc0b87814989efa711f9becd9f26969820e2d3905db27d10969c4bd45890" +dependencies = [ + "anyhow", + "equivalent", + "foyer-common", + "foyer-memory", + "foyer-storage", + "foyer-tokio", + "futures-util", + "mea", + "mixtrics", + "pin-project", + "serde", + "tracing", +] + +[[package]] +name = "foyer-common" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3db80d5dece93adb7ad709c84578794724a9cba342a7e566c3551c7ec626789" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "cfg-if", + "foyer-tokio", + "mixtrics", + "parking_lot", + "pin-project", + "serde", + "twox-hash", +] + +[[package]] +name = "foyer-intrusive-collections" +version = "0.10.0-dev" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4fee46bea69e0596130e3210e65d3424e0ac1e6df3bde6636304bdf1ca4a3b" +dependencies = [ + "memoffset", +] + +[[package]] +name = "foyer-memory" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db907f40a527ca2aa2f40a5f68b32ea58aa70f050cd233518e9ffd402cfba6ce" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "cmsketch", + "equivalent", + "foyer-common", + "foyer-intrusive-collections", + "foyer-tokio", + "futures-util", + "hashbrown 0.16.1", + "itertools 0.14.0", + "mea", + "mixtrics", + "parking_lot", + "paste", + "pin-project", + "serde", + "tracing", +] + +[[package]] +name = "foyer-storage" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1983f1db3d0710e9c9d5fc116d9202dccd41a2d1e032572224f1aff5520aa958" +dependencies = [ + "allocator-api2", + "anyhow", + "bytes", + "core_affinity", + "equivalent", + "fastant", + "foyer-common", + "foyer-memory", + "foyer-tokio", + "fs4", + "futures-core", + "futures-util", + "hashbrown 0.16.1", + "io-uring", + "itertools 0.14.0", + "libc", + "lz4", + "mea", + "parking_lot", + "pin-project", + "rand", + "serde", + "tracing", + "twox-hash", + "zstd", +] + +[[package]] +name = "foyer-tokio" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6577b05a7ffad0db555aedf00bfe52af818220fc4c1c3a7a12520896fc38627" +dependencies = [ + "tokio", +] + +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags 2.11.1", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "io-uring" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0b564323a0fb6d54b864f625ae139de9612e27edb944dda37c109f05aac531" +dependencies = [ + "hashbrown 0.17.0", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror 2.0.18", + "zerocopy", + "zerocopy-derive", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-docrs-wasm" +version = "0.1.0" +dependencies = [ + "md_docrs_proxy", + "rustdoc-types", + "serde_json", +] + +[[package]] +name = "md-docrs-wasm-compare" +version = "0.1.0" +dependencies = [ + "anyhow", + "wasmer", + "wasmtime", +] + +[[package]] +name = "md_docrs_proxy" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "bytes", + "clap", + "foyer", + "lru", + "reqwest", + "rustdoc-types", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "zstd", +] + +[[package]] +name = "mea" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832" +dependencies = [ + "slab", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mixtrics" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb252c728b9d77c6ef9103f0c81524fa0a3d3b161d0a936295d7fbeff6e04c11" +dependencies = [ + "itertools 0.14.0", + "parking_lot", +] + +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.16.1", + "indexmap", + "memchr", + "ruzstd", +] + +[[package]] +name = "object" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63944c133d03f44e75866bbd160b95af0ec3f6a13d936d69d31c81078cbc5baf" +dependencies = [ + "crc32fast", + "hashbrown 0.16.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pulley-interpreter" +version = "44.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df866b7fd522992ccc6682e58b2741cc7972b163b661db24c4328f4c914cb09d" +dependencies = [ + "cranelift-bitset 0.131.0", + "log", + "pulley-macros", + "wasmtime-internal-core 44.0.0", +] + +[[package]] +name = "pulley-macros" +version = "44.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7dfa8354acc622b3857e1bb1a4e4315d3bc1a44ad31d5653c3e87c0da9306d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.0", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "libc" -version = "0.2.185" +name = "region" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2 0.4.3", + "windows-sys 0.52.0", +] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "rend" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] [[package]] -name = "litemap" +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.16.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustdoc-types" +version = "0.57.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6919c49bdd9cca072b3b502d16c073d0a4d3b7283ff6e0c86a5f6e6b07bbfe" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ruzstd" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" +dependencies = [ + "twox-hash", +] [[package]] -name = "lock_api" -version = "0.4.14" +name = "ryu" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ - "scopeguard", + "serde", + "serde_core", ] [[package]] -name = "log" -version = "0.4.29" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] [[package]] -name = "lru" -version = "0.17.0" +name = "serde-wasm-bindgen" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0b564323a0fb6d54b864f625ae139de9612e27edb944dda37c109f05aac531" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ - "hashbrown 0.17.0", + "js-sys", + "serde", + "wasm-bindgen", ] [[package]] -name = "lru-slab" -version = "0.1.2" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] [[package]] -name = "lz4" -version = "1.28.1" +name = "serde_spanned" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "lz4-sys", + "serde_core", ] [[package]] -name = "lz4-sys" -version = "1.11.1+lz4-1.10.0" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "cc", - "libc", + "form_urlencoded", + "itoa", + "ryu", + "serde", ] [[package]] -name = "matchers" -version = "0.2.0" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "regex-automata", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] -name = "matchit" -version = "0.8.4" +name = "sha2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] [[package]] -name = "md-docrs-wasm" -version = "0.1.0" +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "md_docrs_proxy", - "rustdoc-types", - "serde_json", + "lazy_static", ] [[package]] -name = "md_docrs_proxy" -version = "0.1.0" +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" dependencies = [ - "anyhow", - "async-trait", - "axum", "bytes", - "clap", - "foyer", - "lru", - "reqwest", - "rustdoc-types", - "serde", - "serde_json", - "thiserror", - "tokio", - "tower-http", - "tracing", - "tracing-subscriber", - "zstd", + "memmap2 0.6.2", ] [[package]] -name = "mea" -version = "0.6.3" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832" -dependencies = [ - "slab", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "memchr" -version = "2.8.0" +name = "simd-adler32" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "memoffset" -version = "0.9.1" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] -name = "mime" -version = "0.3.17" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "mio" -version = "1.2.0" +name = "small_ctor" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] +checksum = "88414a5ca1f85d82cc34471e975f0f74f6aa54c40f062efa42c0080e7f763f81" [[package]] -name = "mixtrics" -version = "0.2.3" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb252c728b9d77c6ef9103f0c81524fa0a3d3b161d0a936295d7fbeff6e04c11" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ - "itertools", - "parking_lot", + "serde", ] [[package]] -name = "nu-ansi-term" -version = "0.50.3" +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ + "libc", "windows-sys 0.61.2", ] [[package]] -name = "num_cpus" -version = "1.17.0" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "once_cell" -version = "1.21.4" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "parking_lot" -version = "0.12.5" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ - "lock_api", - "parking_lot_core", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "futures-core", ] [[package]] -name = "paste" -version = "1.0.15" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "tar" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] [[package]] -name = "pin-project" -version = "1.1.11" +name = "target-lexicon" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] -name = "pin-project-internal" -version = "1.1.11" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "proc-macro2", - "quote", - "syn", + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "termcolor" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] [[package]] -name = "pkg-config" -version = "0.3.33" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] [[package]] -name = "potential_utf" -version = "0.1.5" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "zerovec", + "thiserror-impl 2.0.18", ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "zerocopy", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "unicode-ident", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "quinn" -version = "0.11.9" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", + "cfg-if", ] [[package]] -name = "quinn-proto" -version = "0.11.14" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", + "displaydoc", + "zerovec", ] [[package]] -name = "quinn-udp" -version = "0.5.14" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", + "tinyvec_macros", ] [[package]] -name = "quote" -version = "1.0.45" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "r-efi" -version = "5.3.0" +name = "tokio" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] [[package]] -name = "rand" -version = "0.9.4" +name = "tokio-macros" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ - "rand_chacha", - "rand_core", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "ppv-lite86", - "rand_core", + "rustls", + "tokio", ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "toml" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "getrandom 0.3.4", + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "bitflags", + "serde_core", ] [[package]] -name = "regex-automata" -version = "0.4.14" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "winnow 1.0.1", ] [[package]] -name = "regex-syntax" -version = "0.8.10" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] -name = "reqwest" -version = "0.12.28" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "base64", - "bytes", "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", + "futures-util", "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", - "tower", - "tower-http", + "tower-layer", "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", + "tracing", ] [[package]] -name = "ring" -version = "0.17.14" +name = "tower-http" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "rustc-hash" -version = "2.1.2" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "rustdoc-types" -version = "0.57.3" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6919c49bdd9cca072b3b502d16c073d0a4d3b7283ff6e0c86a5f6e6b07bbfe" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "serde", - "serde_derive", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rustls" -version = "0.23.38" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "valuable", ] [[package]] -name = "rustls-pki-types" -version = "1.14.0" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "web-time", - "zeroize", + "log", + "once_cell", + "tracing-core", ] [[package]] -name = "rustls-webpki" -version = "0.103.12" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "ryu" -version = "1.0.23" +name = "twox-hash" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +dependencies = [ + "rand", +] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "typenum" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] -name = "serde" -version = "1.0.228" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "serde_core" -version = "1.0.228" +name = "unicode-segmentation" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] -name = "serde_derive" -version = "1.0.228" +name = "unicode-width" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "serde_json" -version = "1.0.149" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] -name = "serde_path_to_error" -version = "0.1.20" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "url" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", - "itoa", - "ryu", + "idna", + "percent-encoding", "serde", ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "shlex" -version = "1.3.0" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "slab" -version = "0.4.12" +name = "uuid" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "small_ctor" -version = "0.1.2" +name = "valuable" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88414a5ca1f85d82cc34471e975f0f74f6aa54c40f062efa42c0080e7f763f81" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "smallvec" -version = "1.15.1" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "socket2" -version = "0.6.3" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "libc", - "windows-sys 0.61.2", + "try-lock", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "strsim" -version = "0.11.1" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] [[package]] -name = "subtle" -version = "2.6.1" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] [[package]] -name = "syn" -version = "2.0.117" +name = "wasm-bindgen" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] -name = "sync_wrapper" -version = "1.0.2" +name = "wasm-bindgen-futures" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "futures-core", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "wasm-bindgen-macro" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", + "wasm-bindgen-shared", ] [[package]] -name = "thiserror" -version = "2.0.18" +name = "wasm-bindgen-shared" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ - "thiserror-impl", + "unicode-ident", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "wasm-compose" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "f05a2b3bad87cc1ce45b63425ec09a854cc4cb369231c9fed1fee31538103efb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", + "wat", ] [[package]] -name = "thread_local" -version = "1.1.9" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "cfg-if", + "leb128fmt", + "wasmparser 0.244.0", ] [[package]] -name = "tinystr" -version = "0.8.3" +name = "wasm-encoder" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" dependencies = [ - "displaydoc", - "zerovec", + "leb128fmt", + "wasmparser 0.246.2", ] [[package]] -name = "tinyvec" -version = "1.11.0" +name = "wasm-encoder" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" dependencies = [ - "tinyvec_macros", + "leb128fmt", + "wasmparser 0.247.0", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] [[package]] -name = "tokio" -version = "1.52.1" +name = "wasmer" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "57bf3ce47ae9ef62e35c0b23e89b72250548d6d960d0832a5638b05a701769a8" dependencies = [ + "bindgen", "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", + "cfg-if", + "cmake", + "derive_more", + "indexmap", + "js-sys", + "more-asserts", + "paste", + "rustc-demangle", + "serde", + "serde-wasm-bindgen", + "shared-buffer", + "tar", + "target-lexicon", + "thiserror 2.0.18", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-compiler-cranelift", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.245.1", + "wat", "windows-sys 0.61.2", ] [[package]] -name = "tokio-macros" -version = "2.7.0" +name = "wasmer-compiler" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "1998787df9de3e84b66616766fb795e9aab954a8ca1e40eb92ef759e325bc782" dependencies = [ - "proc-macro2", - "quote", - "syn", + "backtrace", + "bytes", + "cfg-if", + "crossbeam-channel", + "enum-iterator", + "enumset", + "itertools 0.14.0", + "leb128", + "libc", + "macho-unwind-info", + "memmap2 0.9.10", + "more-asserts", + "object 0.38.1", + "rangemap", + "rayon", + "region", + "rkyv", + "self_cell", + "shared-buffer", + "smallvec", + "target-lexicon", + "tempfile", + "thiserror 2.0.18", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.245.1", + "which", + "windows-sys 0.61.2", ] [[package]] -name = "tokio-rustls" -version = "0.26.4" +name = "wasmer-compiler-cranelift" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "757fd205d4e2aac6662fc75f4859e6675ab51f4436b86086d47bfa83439c8931" dependencies = [ - "rustls", - "tokio", + "cranelift-codegen 0.129.1", + "cranelift-entity 0.129.1", + "cranelift-frontend 0.129.1", + "gimli 0.33.0", + "indexmap", + "itertools 0.14.0", + "leb128", + "more-asserts", + "rayon", + "smallvec", + "target-lexicon", + "tracing", + "wasmer-compiler", + "wasmer-types", ] [[package]] -name = "tower" -version = "0.5.3" +name = "wasmer-derive" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +checksum = "80beffc36bead448bac84e1145d860523729c4e34e441332d56076a8a16b2d64" dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "tower-http" -version = "0.6.8" +name = "wasmer-types" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "c805281d86063190ad76fbc12d6397aaf33ba60a5c9834e98ee0dcb8f889df7d" dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "tracing", + "bytecheck", + "enum-iterator", + "enumset", + "getrandom 0.4.2", + "hex", + "indexmap", + "more-asserts", + "rkyv", + "sha2 0.11.0", + "target-lexicon", + "thiserror 2.0.18", ] [[package]] -name = "tower-layer" -version = "0.3.3" +name = "wasmer-vm" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "2be14431ae698689440eadaa3d9563e266da4f889adda2749743f317680daa20" +dependencies = [ + "backtrace", + "bytesize", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "gimli 0.33.0", + "indexmap", + "itertools 0.14.0", + "libc", + "libunwind", + "mach2 0.6.0", + "memoffset", + "more-asserts", + "parking_lot", + "region", + "rustversion", + "scopeguard", + "thiserror 2.0.18", + "wasmer-types", + "windows-sys 0.61.2", +] [[package]] -name = "tower-service" -version = "0.3.3" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] -name = "tracing" -version = "0.1.44" +name = "wasmparser" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", + "bitflags 2.11.1", +] + +[[package]] +name = "wasmparser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", ] [[package]] -name = "tracing-attributes" -version = "0.1.31" +name = "wasmparser" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" dependencies = [ - "proc-macro2", - "quote", - "syn", + "bitflags 2.11.1", + "indexmap", + "semver", ] [[package]] -name = "tracing-core" -version = "0.1.36" +name = "wasmprinter" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "6e41f7493ba994b8a779430a4c25ff550fd5a40d291693af43a6ef48688f00e3" dependencies = [ - "once_cell", - "valuable", + "anyhow", + "termcolor", + "wasmparser 0.246.2", ] [[package]] -name = "tracing-log" -version = "0.2.0" +name = "wasmtime" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +checksum = "fca3f777dfb4db45915f95eeb25cac7f2eeb268797a27e5eb78b072618135c7f" dependencies = [ + "addr2line 0.26.1", + "async-trait", + "bitflags 2.11.1", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli 0.33.0", + "ittapi", + "libc", "log", + "mach2 0.4.3", + "memfd", + "object 0.39.0", "once_cell", - "tracing-core", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core 44.0.0", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", ] [[package]] -name = "tracing-subscriber" -version = "0.3.23" +name = "wasmtime-environ" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +checksum = "7c5ca1af838cec374931242d07af5d354aedf63f297f95b3625ac863e516ef67" dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", + "anyhow", + "cpp_demangle", + "cranelift-bforest 0.131.0", + "cranelift-bitset 0.131.0", + "cranelift-entity 0.131.0", + "gimli 0.33.0", + "hashbrown 0.16.1", + "indexmap", + "log", + "object 0.39.0", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2 0.10.9", "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", + "target-lexicon", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core 44.0.0", ] [[package]] -name = "try-lock" -version = "0.2.5" +name = "wasmtime-internal-cache" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +checksum = "b2004f7c86ebeb116550655377cdf16dbf7b03ae5aa6b4b1c1458cfa23aaa306" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2 0.10.9", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] [[package]] -name = "twox-hash" -version = "2.1.2" +name = "wasmtime-internal-component-macro" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "58b31927f7b613d8fe019609744e226f6458d8aa5e6289e92fbbc60e521cd026" dependencies = [ - "rand", + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.246.2", ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "wasmtime-internal-component-util" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "dc29e3478928b93979831ba02a997ce7f707c673ce47180d643091cf4fa4f561" [[package]] -name = "untrusted" -version = "0.9.0" +name = "wasmtime-internal-core" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "03a4a3f055a804a2f3d86e816a9df78a8fa57762212a8506164959224a40cd48" +dependencies = [ + "libm", +] [[package]] -name = "url" -version = "2.5.8" +name = "wasmtime-internal-core" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "816a61a75275c6be435131fc625a4f5956daf24d9f9f59443e81cbef228929b3" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", + "anyhow", + "hashbrown 0.16.1", + "libm", "serde", ] [[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" +name = "wasmtime-internal-cranelift" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "69ceb5e079877e7e4565c1e2d86d9db889175d55f7ca0001315576d08c71e634" +dependencies = [ + "cfg-if", + "cranelift-codegen 0.131.0", + "cranelift-control 0.131.0", + "cranelift-entity 0.131.0", + "cranelift-frontend 0.131.0", + "cranelift-native", + "gimli 0.33.0", + "itertools 0.14.0", + "log", + "object 0.39.0", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-core 44.0.0", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] [[package]] -name = "valuable" -version = "0.1.1" +name = "wasmtime-internal-fiber" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "e18f8bb05d25e0d4cca7278147c9f9e2f26f66886ef754b562bf729128f1e537" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] [[package]] -name = "want" -version = "0.3.1" +name = "wasmtime-internal-jit-debug" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "357f1070b31154ee463937b477ca0b2962bf450b40fc59799bef2f656b15da73" dependencies = [ - "try-lock", + "cc", + "object 0.39.0", + "rustix", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "wasmtime-internal-jit-icache-coherence" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "2fd683a94490bf755d016a09697b0955602c50106b1ded97d16983ab2ded9fed" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core 44.0.0", + "windows-sys 0.61.2", +] [[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" +name = "wasmtime-internal-unwinder" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "4471746ce113c3c1862ce2c0674acb35399a4b3ed3ef4531dc087f333c74f064" dependencies = [ - "wit-bindgen", + "cfg-if", + "cranelift-codegen 0.131.0", + "log", + "object 0.39.0", + "wasmtime-environ", ] [[package]] -name = "wasm-bindgen" -version = "0.2.118" +name = "wasmtime-internal-versioned-export-macros" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "d6af582ec18b674bf7a17775d6fbfbddfcc143f0edbd89c9c1778239c8aa92ed" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.68" +name = "wasmtime-internal-winch" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "d31be8916bb60ea756d2f0ae1f634d9258442aa71e773c893e2f4cead30501b5" dependencies = [ - "js-sys", - "wasm-bindgen", + "cranelift-codegen 0.131.0", + "gimli 0.33.0", + "log", + "object 0.39.0", + "target-lexicon", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.118" +name = "wasmtime-internal-wit-bindgen" +version = "44.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "e2150e63d502ab2d64754e5abe8eb737ae674b7dd4ad53144fd16bbeceaf4a19" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "anyhow", + "bitflags 2.11.1", + "heck", + "indexmap", + "wit-parser 0.246.2", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.118" +name = "wast" +version = "247.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "579d2d47eb33b0cdf9b14723cb115f1e1b7d6e77aac6f0816e5b7c7aeaa418ff" dependencies = [ "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.247.0", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.118" +name = "wat" +version = "1.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "f3f4091c56437e86f2b57fa2fac72c4f528957a605b3f44f7c0b3b19a17ac5ee" dependencies = [ - "unicode-ident", + "wast", ] [[package]] @@ -1905,6 +4126,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1921,12 +4151,40 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "44.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9339858ad222412200fd8b1af9e270712201aaec440c7618991443af3446481f" +dependencies = [ + "cranelift-assembler-x64 0.131.0", + "cranelift-codegen 0.131.0", + "gimli 0.33.0", + "regalloc2 0.15.1", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-core 44.0.0", + "wasmtime-internal-cranelift", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -2098,18 +4356,147 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.246.2", +] + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index cf24743..794e592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "rust-wasm"] +members = [".", "rust-wasm", "wasm"] default-members = ["."] resolver = "3" diff --git a/README.md b/README.md index 192df0f..dc58058 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,15 @@ The root crate's HTTP / server / CLI bits are gated behind `http`, `server`, and `cli` features (all on by default), so the pure pipeline compiles for `wasm32` without reqwest/tokio/axum/zstd. +To compare the two modules side by side (size, output parity, per-call +latency) under an embedded wasmtime or wasmer, see +[`wasm/`](wasm/README.md): + +```sh +./wasm/build.sh # builds zig + rust wasm, stages them +cargo run -p md-docrs-wasm-compare # runs the table +``` + ## Logging ```sh diff --git a/wasm/.gitignore b/wasm/.gitignore new file mode 100644 index 0000000..a7fbfa6 --- /dev/null +++ b/wasm/.gitignore @@ -0,0 +1 @@ +artifacts/*.wasm diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..d5f97ef --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "md-docrs-wasm-compare" +version = "0.1.0" +edition = "2024" +publish = false + +[[bin]] +name = "wasm-compare" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +wasmtime = "44" +wasmer = { version = "7", optional = true } + +[features] +default = [] diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000..6131602 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,95 @@ +# wasm/ — side-by-side comparison harness + +Runs the Zig and Rust wasm builds of `resolve_url` through the exact same +sequence of specs and reports: + +- artifact size +- resolved URL (parity check — every artifact must produce byte-identical output) +- median and p95 per-call latency + +Default runtime is embedded **wasmtime** (crate). The `wasmer` cargo feature +swaps in the **wasmer** crate as an alternate host. Both are in-process +embeddings, not the `wasmtime` / `wasmer` CLI binaries. + +## Layout + +``` +wasm/ +├── Cargo.toml # md-docrs-wasm-compare (workspace member) +├── src/main.rs # harness: loads wasm, drives resolve_url, reports +├── build.sh # builds zig + rust wasms and stages them in artifacts/ +├── artifacts/ # .gitignored — populated by build.sh +│ ├── zig.wasm +│ ├── rust-minimal.wasm +│ └── rust-full.wasm +└── README.md +``` + +## Quick start + +```sh +# From repo root. +./wasm/build.sh # produces artifacts/*.wasm +cargo run -p md-docrs-wasm-compare # default: wasmtime, 200 iterations +``` + +Sample output: + +``` +artifact bytes +-------------- ---------- +zig 6775 +rust-minimal 36268 +rust-full 404159 + +spec: tokio@1.52.1::sync::Mutex +artifact output median µs p95 µs +-------------- ------------------------------------------------ --------- ---------- +zig https://docs.rs/crate/tokio/1.52.1/json/57.zst 7 8 +rust-minimal https://docs.rs/crate/tokio/1.52.1/json/57.zst 9 9 +rust-full https://docs.rs/crate/tokio/1.52.1/json/57.zst 9 9 +``` + +All three artifacts must return byte-identical URLs for every spec — that is +the ABI parity check. Per-call latency includes three `alloc`s, one +`resolve_url`, three `free`s, plus one `Memory::write` per input and one +`Memory::read` for the output. + +## Flags + +| Flag | Default | Meaning | +| --- | --- | --- | +| `--runtime wasmtime\|wasmer` | `wasmtime` | Embedded host. `wasmer` requires `--features wasmer`. | +| `--iterations N` | 200 | Hot-loop samples per (artifact, spec) cell. | +| `--artifacts-dir PATH` | `wasm/artifacts` | Where to look for `zig.wasm`, `rust-minimal.wasm`, `rust-full.wasm`. | + +Any of the three `.wasm` files may be missing — the harness just skips that row. + +## Wasmer (optional) + +```sh +cargo run -p md-docrs-wasm-compare --features wasmer -- --runtime wasmer +``` + +Wasmer pulls in its own Cranelift fork; first build is ~20s. Both runtimes +agree on output, but wasmer's singlepass / cranelift defaults typically +give different per-call timings than wasmtime's cranelift — useful for +separating ABI cost from JIT cost. + +## Running the raw `.wasm` without the harness + +The CLI form of wasmtime / wasmer can't easily marshal strings across the +ABI boundary, but you can still inspect the modules: + +```sh +wasmtime compile wasm/artifacts/zig.wasm -o /tmp/zig.cwasm +wasmer inspect wasm/artifacts/rust-minimal.wasm | head +``` + +For an end-to-end call you need host code that writes the spec into WASM +memory and reads the result back — that's exactly what `src/main.rs` does. + +## Adding a new spec + +Edit `DEFAULT_SPECS` in `src/main.rs`. A spec is `(spec_string, optional_target)` +and runs against every `.wasm` in the artifacts directory. diff --git a/wasm/build.sh b/wasm/build.sh new file mode 100755 index 0000000..b848142 --- /dev/null +++ b/wasm/build.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Build the Zig and Rust wasm artifacts and stage them under artifacts/ so +# the comparison harness (cargo run -p md-docrs-wasm-compare) can load them +# without knowing where each toolchain drops its output. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "${HERE}/.." && pwd)" +ARTIFACTS="${HERE}/artifacts" + +mkdir -p "${ARTIFACTS}" + +echo ">> zig: ReleaseSmall, wasm32-freestanding" +(cd "${ROOT}/zig/lib" && zig build) +cp "${ROOT}/zig/lib/zig-out/bin/md-docrs.wasm" "${ARTIFACTS}/zig.wasm" + +echo ">> rust-minimal: wasm-release, --no-default-features (resolve_url only)" +cargo build --manifest-path "${ROOT}/Cargo.toml" \ + --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm --no-default-features +cp "${ROOT}/target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm" \ + "${ARTIFACTS}/rust-minimal.wasm" + +echo ">> rust-full: wasm-release, default features (+render_markdown)" +cargo build --manifest-path "${ROOT}/Cargo.toml" \ + --profile wasm-release --target wasm32-unknown-unknown \ + -p md-docrs-wasm +cp "${ROOT}/target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm" \ + "${ARTIFACTS}/rust-full.wasm" + +echo +echo "staged artifacts:" +ls -la "${ARTIFACTS}" diff --git a/wasm/src/main.rs b/wasm/src/main.rs new file mode 100644 index 0000000..215053b --- /dev/null +++ b/wasm/src/main.rs @@ -0,0 +1,386 @@ +//! Side-by-side comparison harness for the Zig and Rust `resolve_url` wasm +//! builds. Loads each .wasm inside an embedded wasmtime (or wasmer, via the +//! `wasmer` feature) and drives the same sequence of specs through both, +//! reporting artifact size, output parity, and per-call latency. +//! +//! Expects artifacts under `artifacts/` by default. Run `build.sh` first. + +use std::{ + fs, + path::{Path, PathBuf}, + time::{Duration, Instant}, +}; + +use anyhow::{Context, Result, bail}; + +const OUT_CAP: u32 = 512; +const DEFAULT_ITERATIONS: usize = 200; + +#[derive(Clone, Debug)] +struct Spec { + spec: &'static str, + target: Option<&'static str>, +} + +const DEFAULT_SPECS: &[Spec] = &[ + Spec { spec: "serde", target: None }, + Spec { spec: "tokio@1.52.1::sync::Mutex", target: None }, + Spec { spec: "anyhow::Error", target: Some("x86_64-unknown-linux-gnu") }, + Spec { spec: "rustdoc-types@0.57::Crate", target: None }, +]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Runtime { + Wasmtime, + #[cfg(feature = "wasmer")] + Wasmer, +} + +struct Args { + runtime: Runtime, + iterations: usize, + artifacts_dir: PathBuf, +} + +fn parse_args() -> Result { + let mut runtime = Runtime::Wasmtime; + let mut iterations = DEFAULT_ITERATIONS; + let mut artifacts_dir = default_artifacts_dir(); + + let mut iter = std::env::args().skip(1); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--runtime" => { + let v = iter.next().context("--runtime expects a value")?; + runtime = match v.as_str() { + "wasmtime" => Runtime::Wasmtime, + #[cfg(feature = "wasmer")] + "wasmer" => Runtime::Wasmer, + #[cfg(not(feature = "wasmer"))] + "wasmer" => bail!( + "wasmer runtime not compiled in; rebuild with `--features wasmer`" + ), + other => bail!("unknown runtime: {other}"), + }; + } + "--iterations" => { + iterations = iter + .next() + .context("--iterations expects a value")? + .parse() + .context("--iterations must be a positive integer")?; + } + "--artifacts-dir" => { + artifacts_dir = PathBuf::from(iter.next().context("--artifacts-dir expects a value")?); + } + "-h" | "--help" => { + print_help(); + std::process::exit(0); + } + other => bail!("unknown argument: {other}"), + } + } + + Ok(Args { runtime, iterations, artifacts_dir }) +} + +fn print_help() { + println!( + "usage: wasm-compare [--runtime wasmtime|wasmer] [--iterations N] [--artifacts-dir PATH]\n\ + \n\ + Runs the same specs through each .wasm in the artifacts directory and\n\ + reports size, output, and median / p95 call latency.\n\ + \n\ + Artifacts expected under --artifacts-dir (default: wasm/artifacts):\n\ + zig.wasm - Zig ReleaseSmall resolve_url build\n\ + rust-minimal.wasm - Rust wasm-release, --no-default-features\n\ + rust-full.wasm - Rust wasm-release, full pipeline (optional)\n\ + \n\ + Any of the three may be missing; the harness just skips them." + ); +} + +fn default_artifacts_dir() -> PathBuf { + // When invoked via `cargo run -p md-docrs-wasm-compare`, CARGO_MANIFEST_DIR + // points at wasm/, so `artifacts/` is right there. + if let Some(dir) = option_env!("CARGO_MANIFEST_DIR") { + return Path::new(dir).join("artifacts"); + } + PathBuf::from("wasm/artifacts") +} + +fn main() -> Result<()> { + let args = parse_args()?; + + let artifacts = [ + ("zig", args.artifacts_dir.join("zig.wasm")), + ("rust-minimal", args.artifacts_dir.join("rust-minimal.wasm")), + ("rust-full", args.artifacts_dir.join("rust-full.wasm")), + ]; + + let present: Vec<_> = artifacts + .iter() + .filter(|(_, path)| path.exists()) + .collect(); + + if present.is_empty() { + bail!( + "no .wasm artifacts found under {}\n\ + run `{}/build.sh` first, or pass --artifacts-dir", + args.artifacts_dir.display(), + env!("CARGO_MANIFEST_DIR"), + ); + } + + println!("runtime: {:?}", args.runtime); + println!("iterations: {}", args.iterations); + println!("artifacts: {}", args.artifacts_dir.display()); + println!(); + + println!("{:<14} {:>10}", "artifact", "bytes"); + println!("{:-<14} {:->10}", "", ""); + for (label, path) in &present { + let meta = fs::metadata(path)?; + println!("{:<14} {:>10}", label, meta.len()); + } + println!(); + + for spec in DEFAULT_SPECS { + println!( + "spec: {}{}", + spec.spec, + spec.target.map(|t| format!(" (target={t})")).unwrap_or_default(), + ); + println!( + "{:<14} {:<60} {:>10} {:>10}", + "artifact", "output", "median µs", "p95 µs" + ); + println!("{:-<14} {:-<60} {:->10} {:->10}", "", "", "", ""); + for (label, path) in &present { + let bytes = fs::read(path)?; + match run_spec(args.runtime, &bytes, spec, args.iterations) { + Ok(result) => { + let output = result + .output + .as_deref() + .unwrap_or(""); + let shown = if output.len() > 60 { + format!("{}...", &output[..57]) + } else { + output.to_string() + }; + println!( + "{:<14} {:<60} {:>10} {:>10}", + label, + shown, + result.median.as_micros(), + result.p95.as_micros(), + ); + } + Err(e) => println!("{:<14} error: {}", label, e), + } + } + println!(); + } + + Ok(()) +} + +struct RunResult { + output: Option, + median: Duration, + p95: Duration, +} + +fn run_spec(runtime: Runtime, wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { + match runtime { + Runtime::Wasmtime => wasmtime_runner::run(wasm_bytes, spec, iterations), + #[cfg(feature = "wasmer")] + Runtime::Wasmer => wasmer_runner::run(wasm_bytes, spec, iterations), + } +} + +fn stats(mut samples: Vec) -> (Duration, Duration) { + samples.sort(); + let median = samples[samples.len() / 2]; + let p95_idx = ((samples.len() as f64) * 0.95) as usize; + let p95 = samples[p95_idx.min(samples.len() - 1)]; + (median, p95) +} + +mod wasmtime_runner { + use super::*; + use wasmtime::{Engine, Instance, Memory, Module, Store, TypedFunc}; + + pub fn run(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { + let engine = Engine::default(); + let module = Module::new(&engine, wasm_bytes)?; + let mut store = Store::new(&engine, ()); + let instance = Instance::new(&mut store, &module, &[])?; + let memory = instance + .get_memory(&mut store, "memory") + .context("wasm module is missing the `memory` export")?; + let alloc: TypedFunc = + instance.get_typed_func(&mut store, "alloc")?; + let free: TypedFunc<(u32, u32), ()> = + instance.get_typed_func(&mut store, "free")?; + let resolve_url: TypedFunc<(u32, u32, u32, u32, u32, u32), u32> = + instance.get_typed_func(&mut store, "resolve_url")?; + + let first = call(&mut store, memory, &alloc, &free, &resolve_url, spec)?; + + let mut samples = Vec::with_capacity(iterations); + for _ in 0..iterations { + let start = Instant::now(); + let _ = call(&mut store, memory, &alloc, &free, &resolve_url, spec)?; + samples.push(start.elapsed()); + } + let (median, p95) = stats(samples); + + Ok(RunResult { output: first, median, p95 }) + } + + fn call( + store: &mut Store<()>, + memory: Memory, + alloc: &TypedFunc, + free: &TypedFunc<(u32, u32), ()>, + resolve_url: &TypedFunc<(u32, u32, u32, u32, u32, u32), u32>, + spec: &Spec, + ) -> Result> { + let spec_len = spec.spec.len() as u32; + let spec_ptr = alloc.call(&mut *store, spec_len)?; + if spec_ptr == 0 { + bail!("alloc(spec) returned null"); + } + memory.write(&mut *store, spec_ptr as usize, spec.spec.as_bytes())?; + + let (target_ptr, target_len) = if let Some(t) = spec.target { + let p = alloc.call(&mut *store, t.len() as u32)?; + if p == 0 { + bail!("alloc(target) returned null"); + } + memory.write(&mut *store, p as usize, t.as_bytes())?; + (p, t.len() as u32) + } else { + (0, 0) + }; + + let out_ptr = alloc.call(&mut *store, OUT_CAP)?; + if out_ptr == 0 { + bail!("alloc(out) returned null"); + } + + let n = resolve_url.call( + &mut *store, + (spec_ptr, spec_len, target_ptr, target_len, out_ptr, OUT_CAP), + )?; + + let output = if n == 0 { + None + } else { + let mut buf = vec![0u8; n as usize]; + memory.read(&*store, out_ptr as usize, &mut buf)?; + Some(String::from_utf8(buf).context("resolve_url returned non-UTF8 bytes")?) + }; + + free.call(&mut *store, (spec_ptr, spec_len))?; + if target_len > 0 { + free.call(&mut *store, (target_ptr, target_len))?; + } + free.call(&mut *store, (out_ptr, OUT_CAP))?; + + Ok(output) + } +} + +#[cfg(feature = "wasmer")] +mod wasmer_runner { + use super::*; + use wasmer::{Instance, Memory, Module, Store, TypedFunction, imports}; + + pub fn run(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { + let mut store = Store::default(); + let module = Module::new(&store, wasm_bytes)?; + let instance = Instance::new(&mut store, &module, &imports! {})?; + let memory = instance.exports.get_memory("memory")?.clone(); + let alloc: TypedFunction = + instance.exports.get_typed_function(&store, "alloc")?; + let free: TypedFunction<(u32, u32), ()> = + instance.exports.get_typed_function(&store, "free")?; + let resolve_url: TypedFunction<(u32, u32, u32, u32, u32, u32), u32> = + instance.exports.get_typed_function(&store, "resolve_url")?; + + let first = call(&mut store, &memory, &alloc, &free, &resolve_url, spec)?; + + let mut samples = Vec::with_capacity(iterations); + for _ in 0..iterations { + let start = Instant::now(); + let _ = call(&mut store, &memory, &alloc, &free, &resolve_url, spec)?; + samples.push(start.elapsed()); + } + let (median, p95) = stats(samples); + + Ok(RunResult { output: first, median, p95 }) + } + + fn call( + store: &mut Store, + memory: &Memory, + alloc: &TypedFunction, + free: &TypedFunction<(u32, u32), ()>, + resolve_url: &TypedFunction<(u32, u32, u32, u32, u32, u32), u32>, + spec: &Spec, + ) -> Result> { + let spec_len = spec.spec.len() as u32; + let spec_ptr = alloc.call(&mut *store, spec_len)?; + if spec_ptr == 0 { + bail!("alloc(spec) returned null"); + } + memory + .view(&*store) + .write(spec_ptr as u64, spec.spec.as_bytes())?; + + let (target_ptr, target_len) = if let Some(t) = spec.target { + let p = alloc.call(&mut *store, t.len() as u32)?; + if p == 0 { + bail!("alloc(target) returned null"); + } + memory.view(&*store).write(p as u64, t.as_bytes())?; + (p, t.len() as u32) + } else { + (0, 0) + }; + + let out_ptr = alloc.call(&mut *store, OUT_CAP)?; + if out_ptr == 0 { + bail!("alloc(out) returned null"); + } + + let n = resolve_url.call( + &mut *store, + spec_ptr, + spec_len, + target_ptr, + target_len, + out_ptr, + OUT_CAP, + )?; + + let output = if n == 0 { + None + } else { + let mut buf = vec![0u8; n as usize]; + memory.view(&*store).read(out_ptr as u64, &mut buf)?; + Some(String::from_utf8(buf).context("resolve_url returned non-UTF8 bytes")?) + }; + + free.call(&mut *store, spec_ptr, spec_len)?; + if target_len > 0 { + free.call(&mut *store, target_ptr, target_len)?; + } + free.call(&mut *store, out_ptr, OUT_CAP)?; + + Ok(output) + } +} From 26aa5784e46afdb90b6f8efb9144a7edfb9f08b8 Mon Sep 17 00:00:00 2001 From: Thomas Aubry Date: Tue, 21 Apr 2026 10:46:55 +0200 Subject: [PATCH 7/7] Add wasm-opt Rust wasm artifact comparison --- Cargo.lock | 84 +++++ README.md | 6 +- rust-wasm/Cargo.toml | 15 +- rust-wasm/README.md | 54 ++- rust-wasm/src/lib.rs | 205 ++++++++--- wasm/Cargo.toml | 2 + wasm/README.md | 37 +- wasm/build.sh | 45 ++- wasm/src/main.rs | 800 ++++++++++++++++++++++++++++++++++++++----- 9 files changed, 1095 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 372d120..6fceef2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,30 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -240,6 +264,20 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "blake3" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -416,12 +454,35 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "const-oid" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -1953,6 +2014,7 @@ version = "0.1.0" dependencies = [ "md_docrs_proxy", "rustdoc-types", + "ruzstd", "serde_json", ] @@ -1961,6 +2023,8 @@ name = "md-docrs-wasm-compare" version = "0.1.0" dependencies = [ "anyhow", + "blake3", + "reqwest", "wasmer", "wasmtime", ] @@ -2636,7 +2700,9 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -3206,6 +3272,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -3267,13 +3346,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags 2.11.1", "bytes", + "futures-core", "futures-util", "http", "http-body", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", diff --git a/README.md b/README.md index dc58058..5296c38 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ cargo build --profile wasm-release --target wasm32-unknown-unknown \ # Full (adds render_markdown, brings in serde_json + rustdoc-types). cargo build --profile wasm-release --target wasm32-unknown-unknown \ -p md-docrs-wasm +# Optional shipped-size pass for Rust artifacts. +wasm-opt -Oz --strip-debug --strip-dwarf \ + -o wasm/artifacts/rust-minimal-opt.wasm \ + target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm ``` The root crate's HTTP / server / CLI bits are gated behind `http`, `server`, @@ -80,7 +84,7 @@ latency) under an embedded wasmtime or wasmer, see [`wasm/`](wasm/README.md): ```sh -./wasm/build.sh # builds zig + rust wasm, stages them +./wasm/build.sh # builds zig + rust wasm, runs wasm-opt, stages them cargo run -p md-docrs-wasm-compare # runs the table ``` diff --git a/rust-wasm/Cargo.toml b/rust-wasm/Cargo.toml index 3287576..f32348a 100644 --- a/rust-wasm/Cargo.toml +++ b/rust-wasm/Cargo.toml @@ -10,17 +10,20 @@ publish = false crate-type = ["cdylib", "rlib"] [features] -# Default: both surfaces. -# - `resolve_url` alone is the minimal parity build vs Zig (spec parse + URL). -# - `render` pulls in serde_json + rustdoc-types for the full JSON→Markdown -# pipeline. This roughly triples the .wasm size; toggle off for -# size-parity size comparisons. +# Default: render only (host-driven fetch + decompression). Kept on by +# default so `cargo test -p md-docrs-wasm` exercises the renderer. +# - `render` pulls in serde_json + rustdoc-types for JSON -> Markdown. +# - `fetch` adds the host `env.fetch_bytes` import and in-WASM zstd +# decoding (via ruzstd) so the module owns the full pipeline. +# - `full` is the convenience alias used by `wasm/build.sh`. default = ["render"] render = ["dep:serde_json", "dep:rustdoc-types"] +fetch = ["dep:ruzstd"] +full = ["render", "fetch"] [dependencies] # Pulls in only the pure pipeline (spec / resolve / render / cache). md_docrs_proxy = { path = "..", default-features = false } rustdoc-types = { version = "0.57", optional = true } serde_json = { version = "1", optional = true } - +ruzstd = { version = "0.8", optional = true, default-features = false, features = ["std"] } diff --git a/rust-wasm/README.md b/rust-wasm/README.md index 91c5c2e..7c38fe4 100644 --- a/rust-wasm/README.md +++ b/rust-wasm/README.md @@ -20,26 +20,39 @@ without any host-side code changes. # Minimal parity build — matches the Zig wasm surface (resolve_url only). cargo build --profile wasm-release --target wasm32-unknown-unknown \ -p md-docrs-wasm --no-default-features +wasm-opt -Oz --strip-debug --strip-dwarf \ + -o target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.opt.wasm \ + target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm # Full pipeline — adds render_markdown (serde_json + rustdoc-types). cargo build --profile wasm-release --target wasm32-unknown-unknown \ -p md-docrs-wasm +wasm-opt -Oz --strip-debug --strip-dwarf \ + -o target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.opt.wasm \ + target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm ``` -Artifact lives at `target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm`. +Raw artifact lives at `target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm`. + +If you run `wasm-opt`, the optimized artifact can live alongside it, e.g. +`target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.opt.wasm`. ## Size snapshot -Measured on Rust 1.94 / Zig 0.16 with no post-build shrinking (no `wasm-opt`). +Measured on Rust 1.94 / Zig 0.16. | Build | Bytes | | --- | ---: | -| Zig 0.16 — `ReleaseSmall` + `strip`, exports `resolve_url` | *TBD (run `cd zig/lib && zig build && wc -c zig-out/bin/md-docrs.wasm`)* | -| Rust `wasm-release` — `resolve_url` only (`--no-default-features`) | **35,939** | -| Rust `wasm-release` — `resolve_url` + `render_markdown` | **414,560** | +| Zig 0.16 — `ReleaseSmall` + `strip`, exports `resolve_url` | **6,775** | +| Rust `wasm-release` — `resolve_url` only (`--no-default-features`) | **36,336** | +| Rust `wasm-release` + `wasm-opt -Oz` — `resolve_url` only | **28,523** | +| Rust `wasm-release` — `resolve_url` + `render_markdown` | **486,387** | + +For the `resolve_url`-only Rust build, `wasm-opt -Oz` trims about **7,813 bytes** +from the raw `wasm-release` artifact, roughly a **21.5%** reduction. -The ~10x jump for `render_markdown` is serde_json + `rustdoc-types` deserialise -impls. Expected; that's the cost of JSON→AST→Markdown. +The large jump for `render_markdown` is serde_json + `rustdoc-types` +deserialise impls. Expected; that's the cost of JSON→AST→Markdown. ## Feature gates @@ -76,4 +89,29 @@ Rust artifact. or the rustdoc types. - Benchmark instantiation + per-call latency side-by-side in a Worker (e.g. hyperfine-style loop from a test harness, or wrangler dev + `wrk`). -- Optional: run `wasm-opt -Oz` on both artifacts for a true "shipped" size. +- Keep comparing raw vs `wasm-opt -Oz` output as the Rust WASM surface grows, + especially once Zig gains the full render pipeline too. + +Option A: keep `std`, but drastically reduce code size +This is the lowest-risk path. + +For the minimal build: +- stop using `ItemSpec::parse` +- stop using `String` +- stop using `format!` +- implement a tiny local parser over `&[u8]` +- write URL bytes directly to `out_ptr` + +This alone could cut a lot. + +### Option B: create a dedicated `no_std` tiny crate +Example direction: +- `rust-wasm-tiny/` +- exports only `resolve_url` +- parser implemented over raw bytes +- no `std` +- no `serde` +- no `rustdoc-types` +- no dependency on main crate + +This is the path most likely to get you materially closer to Zig. diff --git a/rust-wasm/src/lib.rs b/rust-wasm/src/lib.rs index 5a9bb6e..c6ddb68 100644 --- a/rust-wasm/src/lib.rs +++ b/rust-wasm/src/lib.rs @@ -2,9 +2,22 @@ //! //! Exposes the same `alloc` / `free` / `resolve_url` trio as the Zig build //! (`zig/lib/wasm.zig`) so both modules are drop-in interchangeable behind -//! the same Cloudflare Worker. Adds `render_markdown`, which takes an already -//! decoded rustdoc JSON blob plus a spec and returns rendered Markdown — the -//! piece the Zig side will eventually mirror for the full-pipeline benchmark. +//! the same Cloudflare Worker. Adds `render_markdown` (host-fed JSON) and +//! `render_spec` (host-imported fetch + in-WASM zstd decode + render) for +//! the full pipeline comparison. +//! +//! ## Error codes returned by `render_spec` +//! The Zig full WASM build uses the same values. +//! +//! | code | meaning | +//! | ---- | ------- | +//! | 0 | success (ptr + len written to out-slots) | +//! | -1 | alloc failure | +//! | -2 | host `fetch_bytes` returned non-zero | +//! | -3 | zstd decode failure | +//! | -4 | JSON parse failure | +//! | -5 | spec parse / resolve miss / URL too long | +//! | -6 | output pointer write failure | use md_docrs_proxy::ItemSpec; #[cfg(feature = "render")] @@ -31,7 +44,6 @@ pub extern "C" fn alloc(len: u32) -> *mut u8 { let Some(layout) = layout_for(len as usize) else { return ptr::null_mut(); }; - // SAFETY: layout has non-zero size (checked above). unsafe { rust_alloc(layout) } } @@ -50,6 +62,36 @@ pub unsafe extern "C" fn free(ptr: *mut u8, len: u32) { unsafe { dealloc(ptr, layout) }; } +fn build_docs_rs_url(spec: &ItemSpec) -> String { + match spec.target.as_deref() { + Some(t) => format!( + "{DOCS_RS_BASE}/crate/{}/{}/{}/json/{FORMAT_VERSION}.zst", + spec.crate_name, spec.version, t + ), + None => format!( + "{DOCS_RS_BASE}/crate/{}/{}/json/{FORMAT_VERSION}.zst", + spec.crate_name, spec.version + ), + } +} + +fn parse_spec_with_target( + spec_ptr: *const u8, + spec_len: u32, + target_ptr: *const u8, + target_len: u32, +) -> Option { + let spec_bytes = unsafe { slice::from_raw_parts(spec_ptr, spec_len as usize) }; + let spec_str = std::str::from_utf8(spec_bytes).ok()?; + let mut spec = ItemSpec::parse(spec_str).ok()?; + if target_len > 0 { + let t = unsafe { slice::from_raw_parts(target_ptr, target_len as usize) }; + let t_str = std::str::from_utf8(t).ok()?; + spec = spec.with_target(Some(t_str.to_string())); + } + Some(spec) +} + /// Parse `spec` and write the docs.rs rustdoc JSON URL into `out_ptr`. /// `target_len == 0` means "use the default host target". /// Returns bytes written, or 0 on any failure. @@ -66,33 +108,10 @@ pub unsafe extern "C" fn resolve_url( out_ptr: *mut u8, out_cap: u32, ) -> u32 { - let spec_bytes = unsafe { slice::from_raw_parts(spec_ptr, spec_len as usize) }; - let Ok(spec_str) = std::str::from_utf8(spec_bytes) else { - return 0; - }; - let Ok(mut spec) = ItemSpec::parse(spec_str) else { + let Some(spec) = parse_spec_with_target(spec_ptr, spec_len, target_ptr, target_len) else { return 0; }; - - if target_len > 0 { - let t = unsafe { slice::from_raw_parts(target_ptr, target_len as usize) }; - let Ok(t_str) = std::str::from_utf8(t) else { - return 0; - }; - spec = spec.with_target(Some(t_str.to_string())); - } - - let url = match spec.target.as_deref() { - Some(t) => format!( - "{DOCS_RS_BASE}/crate/{}/{}/{}/json/{FORMAT_VERSION}.zst", - spec.crate_name, spec.version, t - ), - None => format!( - "{DOCS_RS_BASE}/crate/{}/{}/json/{FORMAT_VERSION}.zst", - spec.crate_name, spec.version - ), - }; - + let url = build_docs_rs_url(&spec); let bytes = url.as_bytes(); if bytes.len() > out_cap as usize { return 0; @@ -114,9 +133,6 @@ pub unsafe extern "C" fn resolve_url( /// miss, alloc failure). `*len_out` is only meaningful when the return /// value is non-null. /// -/// Output length is not known in advance (varies with the item's doc size) -/// so we allocate here rather than asking the caller to guess a bound. -/// /// # Safety /// All input (ptr, len) pairs must describe valid readable slices. `len_out` /// must be a writable `u32`. @@ -132,21 +148,9 @@ pub unsafe extern "C" fn render_markdown( len_out: *mut u32, ) -> *mut u8 { let json = unsafe { slice::from_raw_parts(json_ptr, json_len as usize) }; - let spec_bytes = unsafe { slice::from_raw_parts(spec_ptr, spec_len as usize) }; - - let Ok(spec_str) = std::str::from_utf8(spec_bytes) else { + let Some(spec) = parse_spec_with_target(spec_ptr, spec_len, target_ptr, target_len) else { return ptr::null_mut(); }; - let Ok(mut spec) = ItemSpec::parse(spec_str) else { - return ptr::null_mut(); - }; - if target_len > 0 { - let t = unsafe { slice::from_raw_parts(target_ptr, target_len as usize) }; - let Ok(t_str) = std::str::from_utf8(t) else { - return ptr::null_mut(); - }; - spec = spec.with_target(Some(t_str.to_string())); - } let Ok(krate) = serde_json::from_slice::(json) else { return ptr::null_mut(); @@ -160,7 +164,6 @@ pub unsafe extern "C" fn render_markdown( let Some(layout) = layout_for(bytes.len()) else { return ptr::null_mut(); }; - // SAFETY: non-zero layout. let out = unsafe { rust_alloc(layout) }; if out.is_null() { return ptr::null_mut(); @@ -172,6 +175,115 @@ pub unsafe extern "C" fn render_markdown( out } +// --------------------------------------------------------------------------- +// Full-pipeline entry. Imports `env.fetch_bytes` from the host and decodes +// the zstd-compressed rustdoc JSON in-module. +// --------------------------------------------------------------------------- + +#[cfg(all(feature = "render", feature = "fetch"))] +unsafe extern "C" { + /// Host-provided: perform an HTTP GET against `url` and hand back the raw + /// response body (still zstd-compressed, as served by docs.rs). + /// + /// The host allocates the destination buffer inside WASM memory by + /// calling the exported `alloc`, writes the body into it, then stores the + /// pointer at `*buf_ptr_out` and the length at `*buf_len_out`. + /// Returns 0 on success, non-zero on any HTTP / transport failure (no + /// buffer written in that case). + fn fetch_bytes( + url_ptr: *const u8, + url_len: u32, + buf_ptr_out: *mut u32, + buf_len_out: *mut u32, + ) -> i32; +} + +#[cfg(all(feature = "render", feature = "fetch"))] +fn zstd_decode(input: &[u8]) -> Option> { + use ruzstd::decoding::StreamingDecoder; + use std::io::Read; + + let mut decoder = StreamingDecoder::new(input).ok()?; + let mut out = Vec::with_capacity(input.len() * 4); + decoder.read_to_end(&mut out).ok()?; + Some(out) +} + +/// Full pipeline: parse spec → fetch via host → zstd decode → JSON parse → +/// resolve → render. On success writes `(ptr, len)` of an `alloc`-owned +/// Markdown buffer into `*buf_ptr_out` / `*buf_len_out` and returns 0. +/// +/// See module docs for error codes. +/// +/// # Safety +/// All (ptr, len) pairs must describe valid slices; both `*_out` pointers +/// must reference writable `u32` slots inside WASM linear memory. +#[cfg(all(feature = "render", feature = "fetch"))] +#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))] +pub unsafe extern "C" fn render_spec( + spec_ptr: *const u8, + spec_len: u32, + target_ptr: *const u8, + target_len: u32, + buf_ptr_out: *mut u32, + buf_len_out: *mut u32, +) -> i32 { + let Some(spec) = parse_spec_with_target(spec_ptr, spec_len, target_ptr, target_len) else { + return -5; + }; + let url = build_docs_rs_url(&spec); + + let mut resp_ptr: u32 = 0; + let mut resp_len: u32 = 0; + let rc = unsafe { + fetch_bytes( + url.as_ptr(), + url.len() as u32, + &mut resp_ptr, + &mut resp_len, + ) + }; + if rc != 0 { + return -2; + } + if resp_ptr == 0 || resp_len == 0 { + return -2; + } + + // Take ownership of the host-written buffer; free it once decoded. + let compressed = unsafe { slice::from_raw_parts(resp_ptr as *const u8, resp_len as usize) }; + let decoded = zstd_decode(compressed); + unsafe { free(resp_ptr as *mut u8, resp_len) }; + let Some(json) = decoded else { + return -3; + }; + + let Ok(krate) = serde_json::from_slice::(&json) else { + return -4; + }; + drop(json); + + let Ok(resolved) = resolve::resolve(&krate, &spec) else { + return -5; + }; + let md = render::render(&krate, &resolved, &spec); + + let bytes = md.as_bytes(); + let Some(layout) = layout_for(bytes.len()) else { + return -1; + }; + let out = unsafe { rust_alloc(layout) }; + if out.is_null() { + return -1; + } + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), out, bytes.len()); + *buf_ptr_out = out as u32; + *buf_len_out = bytes.len() as u32; + } + 0 +} + #[cfg(test)] mod tests { use super::*; @@ -261,4 +373,5 @@ mod tests { free(ptr, 64); } } + } diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index d5f97ef..512555d 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -10,6 +10,8 @@ path = "src/main.rs" [dependencies] anyhow = "1" +blake3 = "1" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls", "gzip"] } wasmtime = "44" wasmer = { version = "7", optional = true } diff --git a/wasm/README.md b/wasm/README.md index 6131602..c910518 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -6,6 +6,7 @@ sequence of specs and reports: - artifact size - resolved URL (parity check — every artifact must produce byte-identical output) - median and p95 per-call latency +- raw Rust vs `wasm-opt -Oz` size comparison for the same build flavor Default runtime is embedded **wasmtime** (crate). The `wasmer` cargo feature swaps in the **wasmer** crate as an alternate host. Both are in-process @@ -19,9 +20,11 @@ wasm/ ├── src/main.rs # harness: loads wasm, drives resolve_url, reports ├── build.sh # builds zig + rust wasms and stages them in artifacts/ ├── artifacts/ # .gitignored — populated by build.sh -│ ├── zig.wasm +│ ├── zig-minimal.wasm │ ├── rust-minimal.wasm -│ └── rust-full.wasm +│ ├── rust-minimal-opt.wasm +│ ├── rust-full.wasm +│ └── rust-full-opt.wasm └── README.md ``` @@ -36,11 +39,13 @@ cargo run -p md-docrs-wasm-compare # default: wasmtime, 200 iterations Sample output: ``` -artifact bytes --------------- ---------- -zig 6775 -rust-minimal 36268 -rust-full 404159 +artifact bytes flavor +-------------- ---------- -------- +zig-minimal 6775 minimal +rust-minimal 36336 minimal +rust-minimal-opt 25541 minimal +rust-full 486387 full +rust-full-opt 361606 full spec: tokio@1.52.1::sync::Mutex artifact output median µs p95 µs @@ -61,9 +66,9 @@ the ABI parity check. Per-call latency includes three `alloc`s, one | --- | --- | --- | | `--runtime wasmtime\|wasmer` | `wasmtime` | Embedded host. `wasmer` requires `--features wasmer`. | | `--iterations N` | 200 | Hot-loop samples per (artifact, spec) cell. | -| `--artifacts-dir PATH` | `wasm/artifacts` | Where to look for `zig.wasm`, `rust-minimal.wasm`, `rust-full.wasm`. | +| `--artifacts-dir PATH` | `wasm/artifacts` | Where to look for `zig-minimal.wasm`, `zig-full.wasm`, `rust-minimal.wasm`, `rust-minimal-opt.wasm`, `rust-full.wasm`, and `rust-full-opt.wasm`. | -Any of the three `.wasm` files may be missing — the harness just skips that row. +Any subset of the expected `.wasm` files may be missing — the harness just skips those rows. ## Wasmer (optional) @@ -93,3 +98,17 @@ memory and reads the result back — that's exactly what `src/main.rs` does. Edit `DEFAULT_SPECS` in `src/main.rs`. A spec is `(spec_string, optional_target)` and runs against every `.wasm` in the artifacts directory. + +## wasm-opt outputs + +`build.sh` now requires `wasm-opt` on `PATH` and stages optimized Rust artifacts +next to the raw cargo outputs: + +- `rust-minimal.wasm` — `cargo build --profile wasm-release --no-default-features` +- `rust-minimal-opt.wasm` — same module after `wasm-opt -Oz --strip-debug --strip-dwarf` +- `rust-full.wasm` — `cargo build --profile wasm-release --no-default-features --features full` +- `rust-full-opt.wasm` — same module after `wasm-opt -Oz --strip-debug --strip-dwarf` + +That lets the harness report the size delta between the unoptimized Rust wasm +and the post-processed `wasm-opt` version while still checking `resolve_url` +output parity across all staged artifacts. diff --git a/wasm/build.sh b/wasm/build.sh index b848142..6f78d20 100755 --- a/wasm/build.sh +++ b/wasm/build.sh @@ -2,6 +2,14 @@ # Build the Zig and Rust wasm artifacts and stage them under artifacts/ so # the comparison harness (cargo run -p md-docrs-wasm-compare) can load them # without knowing where each toolchain drops its output. +# +# Produces up to six artifacts: +# zig-minimal.wasm Zig ReleaseSmall, resolve_url only +# zig-full.wasm Zig ReleaseSmall, full pipeline (if -Dfull supported) +# rust-minimal.wasm Rust wasm-release, --no-default-features +# rust-minimal-opt.wasm Rust wasm-release + wasm-opt -Oz, --no-default-features +# rust-full.wasm Rust wasm-release, --features full (fetch + render) +# rust-full-opt.wasm Rust wasm-release + wasm-opt -Oz, --features full set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -10,9 +18,36 @@ ARTIFACTS="${HERE}/artifacts" mkdir -p "${ARTIFACTS}" -echo ">> zig: ReleaseSmall, wasm32-freestanding" +if command -v wasm-opt >/dev/null 2>&1; then + WASM_OPT="$(command -v wasm-opt)" + echo ">> wasm-opt: ${WASM_OPT}" +else + echo "wasm-opt not found in PATH; install Binaryen to produce optimized Rust artifacts" >&2 + exit 1 +fi + +optimize_wasm() { + local src="$1" + local dest="$2" + + "${WASM_OPT}" -Oz --enable-bulk-memory --strip-debug --strip-dwarf -o "${dest}" "${src}" +} + +echo ">> zig-minimal: ReleaseSmall, wasm32-freestanding" (cd "${ROOT}/zig/lib" && zig build) -cp "${ROOT}/zig/lib/zig-out/bin/md-docrs.wasm" "${ARTIFACTS}/zig.wasm" +cp "${ROOT}/zig/lib/zig-out/bin/md-docrs.wasm" "${ARTIFACTS}/zig-minimal.wasm" + +echo ">> zig-full: ReleaseSmall + full pipeline (-Dfull)" +if (cd "${ROOT}/zig/lib" && zig build -Dfull 2>/dev/null); then + if [[ -f "${ROOT}/zig/lib/zig-out/bin/md-docrs-full.wasm" ]]; then + cp "${ROOT}/zig/lib/zig-out/bin/md-docrs-full.wasm" \ + "${ARTIFACTS}/zig-full.wasm" + else + echo " (skipping: -Dfull accepted but produced no md-docrs-full.wasm)" + fi +else + echo " (skipping: zig -Dfull not supported yet; implement render_spec in zig/lib/)" +fi echo ">> rust-minimal: wasm-release, --no-default-features (resolve_url only)" cargo build --manifest-path "${ROOT}/Cargo.toml" \ @@ -20,13 +55,15 @@ cargo build --manifest-path "${ROOT}/Cargo.toml" \ -p md-docrs-wasm --no-default-features cp "${ROOT}/target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm" \ "${ARTIFACTS}/rust-minimal.wasm" +optimize_wasm "${ARTIFACTS}/rust-minimal.wasm" "${ARTIFACTS}/rust-minimal-opt.wasm" -echo ">> rust-full: wasm-release, default features (+render_markdown)" +echo ">> rust-full: wasm-release, --features full (fetch + render)" cargo build --manifest-path "${ROOT}/Cargo.toml" \ --profile wasm-release --target wasm32-unknown-unknown \ - -p md-docrs-wasm + -p md-docrs-wasm --no-default-features --features full cp "${ROOT}/target/wasm32-unknown-unknown/wasm-release/md_docrs_wasm.wasm" \ "${ARTIFACTS}/rust-full.wasm" +optimize_wasm "${ARTIFACTS}/rust-full.wasm" "${ARTIFACTS}/rust-full-opt.wasm" echo echo "staged artifacts:" diff --git a/wasm/src/main.rs b/wasm/src/main.rs index 215053b..b6fc1d7 100644 --- a/wasm/src/main.rs +++ b/wasm/src/main.rs @@ -1,20 +1,33 @@ -//! Side-by-side comparison harness for the Zig and Rust `resolve_url` wasm -//! builds. Loads each .wasm inside an embedded wasmtime (or wasmer, via the -//! `wasmer` feature) and drives the same sequence of specs through both, -//! reporting artifact size, output parity, and per-call latency. +//! Side-by-side comparison harness for the Zig and Rust WASM builds. //! -//! Expects artifacts under `artifacts/` by default. Run `build.sh` first. +//! Exercises two code paths on every artifact that exports them: +//! +//! - `resolve_url` (minimal column) — spec -> docs.rs URL, size/latency only. +//! - `render_spec` (full column) — spec -> fetched rustdoc JSON -> Markdown. +//! The host provides `env.fetch_bytes` (blocking reqwest), then the guest +//! owns the zstd decode + JSON parse + render. +//! +//! Artifacts expected under `artifacts/` (populated by `build.sh`): +//! zig-minimal.wasm, zig-full.wasm, rust-minimal.wasm, rust-full.wasm +//! +//! Missing artifacts are skipped. `--offline` disables the render column so +//! the harness runs without network. +//! +//! Parity: for each spec, the first full-pipeline output is hashed (blake3) +//! and compared across artifacts. use std::{ + collections::HashMap, fs, path::{Path, PathBuf}, - time::{Duration, Instant}, + time::Duration, }; use anyhow::{Context, Result, bail}; const OUT_CAP: u32 = 512; const DEFAULT_ITERATIONS: usize = 200; +const DEFAULT_RENDER_ITERATIONS: usize = 3; #[derive(Clone, Debug)] struct Spec { @@ -23,10 +36,22 @@ struct Spec { } const DEFAULT_SPECS: &[Spec] = &[ - Spec { spec: "serde", target: None }, - Spec { spec: "tokio@1.52.1::sync::Mutex", target: None }, - Spec { spec: "anyhow::Error", target: Some("x86_64-unknown-linux-gnu") }, - Spec { spec: "rustdoc-types@0.57::Crate", target: None }, + Spec { + spec: "serde", + target: None, + }, + Spec { + spec: "tokio@1.52.1::sync::Mutex", + target: None, + }, + Spec { + spec: "anyhow::Error", + target: Some("x86_64-unknown-linux-gnu"), + }, + Spec { + spec: "rustdoc-types@0.57::Crate", + target: None, + }, ]; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -36,16 +61,32 @@ enum Runtime { Wasmer, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Flavor { + Minimal, + Full, +} + +struct Artifact { + label: &'static str, + path: PathBuf, + flavor: Flavor, +} + struct Args { runtime: Runtime, iterations: usize, + render_iterations: usize, artifacts_dir: PathBuf, + offline: bool, } fn parse_args() -> Result { let mut runtime = Runtime::Wasmtime; let mut iterations = DEFAULT_ITERATIONS; + let mut render_iterations = DEFAULT_RENDER_ITERATIONS; let mut artifacts_dir = default_artifacts_dir(); + let mut offline = false; let mut iter = std::env::args().skip(1); while let Some(arg) = iter.next() { @@ -57,9 +98,9 @@ fn parse_args() -> Result { #[cfg(feature = "wasmer")] "wasmer" => Runtime::Wasmer, #[cfg(not(feature = "wasmer"))] - "wasmer" => bail!( - "wasmer runtime not compiled in; rebuild with `--features wasmer`" - ), + "wasmer" => { + bail!("wasmer runtime not compiled in; rebuild with `--features wasmer`") + } other => bail!("unknown runtime: {other}"), }; } @@ -70,9 +111,18 @@ fn parse_args() -> Result { .parse() .context("--iterations must be a positive integer")?; } + "--render-iterations" => { + render_iterations = iter + .next() + .context("--render-iterations expects a value")? + .parse() + .context("--render-iterations must be a positive integer")?; + } "--artifacts-dir" => { - artifacts_dir = PathBuf::from(iter.next().context("--artifacts-dir expects a value")?); + artifacts_dir = + PathBuf::from(iter.next().context("--artifacts-dir expects a value")?); } + "--offline" => offline = true, "-h" | "--help" => { print_help(); std::process::exit(0); @@ -81,28 +131,38 @@ fn parse_args() -> Result { } } - Ok(Args { runtime, iterations, artifacts_dir }) + Ok(Args { + runtime, + iterations, + render_iterations, + artifacts_dir, + offline, + }) } fn print_help() { println!( - "usage: wasm-compare [--runtime wasmtime|wasmer] [--iterations N] [--artifacts-dir PATH]\n\ - \n\ - Runs the same specs through each .wasm in the artifacts directory and\n\ - reports size, output, and median / p95 call latency.\n\ + "usage: wasm-compare [--runtime wasmtime|wasmer]\n\ + \t[--iterations N] [--render-iterations N]\n\ + \t[--artifacts-dir PATH] [--offline]\n\ \n\ - Artifacts expected under --artifacts-dir (default: wasm/artifacts):\n\ - zig.wasm - Zig ReleaseSmall resolve_url build\n\ - rust-minimal.wasm - Rust wasm-release, --no-default-features\n\ - rust-full.wasm - Rust wasm-release, full pipeline (optional)\n\ + Reports size + median/p95 latency for each WASM artifact under\n\ + --artifacts-dir (default: wasm/artifacts). Runs resolve_url for all\n\ + artifacts; runs the full render_spec pipeline for artifacts that\n\ + export it. --offline skips the render column (no network).\n\ \n\ - Any of the three may be missing; the harness just skips them." + Expected artifacts:\n\ + \tzig-minimal.wasm Zig ReleaseSmall, resolve_url only\n\ + \tzig-full.wasm Zig ReleaseSmall, full pipeline\n\ + \trust-minimal.wasm Rust wasm-release, --no-default-features\n\ + \trust-minimal-opt.wasm Rust wasm-release + wasm-opt -Oz, --no-default-features\n\ + \trust-full.wasm Rust wasm-release, --features full\n\ + \trust-full-opt.wasm Rust wasm-release + wasm-opt -Oz, --features full\n\ + Any subset is fine; missing artifacts are skipped." ); } fn default_artifacts_dir() -> PathBuf { - // When invoked via `cargo run -p md-docrs-wasm-compare`, CARGO_MANIFEST_DIR - // points at wasm/, so `artifacts/` is right there. if let Some(dir) = option_env!("CARGO_MANIFEST_DIR") { return Path::new(dir).join("artifacts"); } @@ -112,17 +172,40 @@ fn default_artifacts_dir() -> PathBuf { fn main() -> Result<()> { let args = parse_args()?; - let artifacts = [ - ("zig", args.artifacts_dir.join("zig.wasm")), - ("rust-minimal", args.artifacts_dir.join("rust-minimal.wasm")), - ("rust-full", args.artifacts_dir.join("rust-full.wasm")), + let all = [ + Artifact { + label: "zig-minimal", + path: args.artifacts_dir.join("zig-minimal.wasm"), + flavor: Flavor::Minimal, + }, + Artifact { + label: "zig-full", + path: args.artifacts_dir.join("zig-full.wasm"), + flavor: Flavor::Full, + }, + Artifact { + label: "rust-minimal", + path: args.artifacts_dir.join("rust-minimal.wasm"), + flavor: Flavor::Minimal, + }, + Artifact { + label: "rust-minimal-opt", + path: args.artifacts_dir.join("rust-minimal-opt.wasm"), + flavor: Flavor::Minimal, + }, + Artifact { + label: "rust-full", + path: args.artifacts_dir.join("rust-full.wasm"), + flavor: Flavor::Full, + }, + Artifact { + label: "rust-full-opt", + path: args.artifacts_dir.join("rust-full-opt.wasm"), + flavor: Flavor::Full, + }, ]; - let present: Vec<_> = artifacts - .iter() - .filter(|(_, path)| path.exists()) - .collect(); - + let present: Vec<_> = all.iter().filter(|a| a.path.exists()).collect(); if present.is_empty() { bail!( "no .wasm artifacts found under {}\n\ @@ -133,15 +216,26 @@ fn main() -> Result<()> { } println!("runtime: {:?}", args.runtime); - println!("iterations: {}", args.iterations); + println!( + "iterations: resolve_url={}, render_spec={}", + args.iterations, args.render_iterations + ); println!("artifacts: {}", args.artifacts_dir.display()); + println!( + "mode: {}", + if args.offline { "offline" } else { "online" } + ); println!(); - println!("{:<14} {:>10}", "artifact", "bytes"); - println!("{:-<14} {:->10}", "", ""); - for (label, path) in &present { - let meta = fs::metadata(path)?; - println!("{:<14} {:>10}", label, meta.len()); + println!("{:<14} {:>10} {:>8}", "artifact", "bytes", "flavor"); + println!("{:-<14} {:->10} {:->8}", "", "", ""); + for a in &present { + let meta = fs::metadata(&a.path)?; + let flavor = match a.flavor { + Flavor::Minimal => "minimal", + Flavor::Full => "full", + }; + println!("{:<14} {:>10} {:>8}", a.label, meta.len(), flavor); } println!(); @@ -149,54 +243,140 @@ fn main() -> Result<()> { println!( "spec: {}{}", spec.spec, - spec.target.map(|t| format!(" (target={t})")).unwrap_or_default(), + spec.target + .map(|t| format!(" (target={t})")) + .unwrap_or_default(), ); println!( "{:<14} {:<60} {:>10} {:>10}", - "artifact", "output", "median µs", "p95 µs" + "artifact", "resolve_url output", "median us", "p95 us" ); println!("{:-<14} {:-<60} {:->10} {:->10}", "", "", "", ""); - for (label, path) in &present { - let bytes = fs::read(path)?; - match run_spec(args.runtime, &bytes, spec, args.iterations) { + for a in &present { + let bytes = fs::read(&a.path)?; + match run_resolve(args.runtime, &bytes, spec, args.iterations) { Ok(result) => { let output = result .output .as_deref() .unwrap_or(""); - let shown = if output.len() > 60 { - format!("{}...", &output[..57]) - } else { - output.to_string() - }; + let shown = truncate(output, 60); println!( "{:<14} {:<60} {:>10} {:>10}", - label, + a.label, shown, result.median.as_micros(), result.p95.as_micros(), ); } - Err(e) => println!("{:<14} error: {}", label, e), + Err(e) => println!("{:<14} resolve_url error: {}", a.label, e), + } + } + + if !args.offline && present.iter().any(|a| a.flavor == Flavor::Full) { + println!(); + println!( + "{:<14} {:>8} {:>8} {:>10} {:>10} {:<16}", + "artifact", "md bytes", "fetch ms", "render ms", "total ms", "parity" + ); + println!( + "{:-<14} {:->8} {:->8} {:->10} {:->10} {:-<16}", + "", "", "", "", "", "" + ); + let mut parity: HashMap> = HashMap::new(); + for a in &present { + if a.flavor != Flavor::Full { + continue; + } + let bytes = fs::read(&a.path)?; + match run_render(args.runtime, &bytes, spec, args.render_iterations) { + Ok(r) => { + let hash = blake3::hash(r.output.as_bytes()); + let short = short_hash(hash.to_hex().as_str()); + parity.entry(short.clone()).or_default().push(a.label); + println!( + "{:<14} {:>8} {:>8} {:>10} {:>10} {:<16}", + a.label, + r.output.len(), + r.fetch_median.as_millis(), + r.render_median.as_millis(), + r.total_median.as_millis(), + short, + ); + } + Err(e) => println!("{:<14} render_spec error: {}", a.label, e), + } + } + if parity.len() > 1 { + println!("parity: outputs differ across artifacts"); + for (hash, labels) in &parity { + println!(" {}: {}", hash, labels.join(", ")); + } + } else if let Some((hash, labels)) = parity.iter().next() { + if labels.len() > 1 { + println!( + "parity: all {} full artifacts agree ({})", + labels.len(), + hash + ); + } } } + println!(); } Ok(()) } -struct RunResult { +fn truncate(s: &str, max: usize) -> String { + if s.len() > max { + format!("{}...", &s[..max.saturating_sub(3)]) + } else { + s.to_string() + } +} + +fn short_hash(hex: &str) -> String { + hex.chars().take(12).collect() +} + +struct ResolveResult { output: Option, median: Duration, p95: Duration, } -fn run_spec(runtime: Runtime, wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { +struct RenderResult { + output: String, + fetch_median: Duration, + render_median: Duration, + total_median: Duration, +} + +fn run_resolve( + runtime: Runtime, + wasm_bytes: &[u8], + spec: &Spec, + iterations: usize, +) -> Result { + match runtime { + Runtime::Wasmtime => wasmtime_runner::run_resolve(wasm_bytes, spec, iterations), + #[cfg(feature = "wasmer")] + Runtime::Wasmer => wasmer_runner::run_resolve(wasm_bytes, spec, iterations), + } +} + +fn run_render( + runtime: Runtime, + wasm_bytes: &[u8], + spec: &Spec, + iterations: usize, +) -> Result { match runtime { - Runtime::Wasmtime => wasmtime_runner::run(wasm_bytes, spec, iterations), + Runtime::Wasmtime => wasmtime_runner::run_render(wasm_bytes, spec, iterations), #[cfg(feature = "wasmer")] - Runtime::Wasmer => wasmer_runner::run(wasm_bytes, spec, iterations), + Runtime::Wasmer => wasmer_runner::run_render(wasm_bytes, spec, iterations), } } @@ -208,40 +388,228 @@ fn stats(mut samples: Vec) -> (Duration, Duration) { (median, p95) } +fn median_duration(mut samples: Vec) -> Duration { + samples.sort(); + samples[samples.len() / 2] +} + +fn blocking_http_client() -> Result { + reqwest::blocking::Client::builder() + .user_agent(concat!("md-docrs-wasm-compare/", env!("CARGO_PKG_VERSION"),)) + .timeout(Duration::from_secs(30)) + .build() + .context("failed to build HTTP client") +} + mod wasmtime_runner { use super::*; - use wasmtime::{Engine, Instance, Memory, Module, Store, TypedFunc}; + use std::sync::{Arc, Mutex}; + use std::time::Instant; + use wasmtime::{Caller, Engine, Linker, Memory, Module, Store, TypedFunc}; - pub fn run(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { + pub fn run_resolve(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { let engine = Engine::default(); let module = Module::new(&engine, wasm_bytes)?; + let linker = build_linker::<()>(&engine)?; let mut store = Store::new(&engine, ()); - let instance = Instance::new(&mut store, &module, &[])?; + let instance = linker.instantiate(&mut store, &module)?; + let memory = instance .get_memory(&mut store, "memory") .context("wasm module is missing the `memory` export")?; - let alloc: TypedFunc = - instance.get_typed_func(&mut store, "alloc")?; - let free: TypedFunc<(u32, u32), ()> = - instance.get_typed_func(&mut store, "free")?; + let alloc: TypedFunc = instance.get_typed_func(&mut store, "alloc")?; + let free: TypedFunc<(u32, u32), ()> = instance.get_typed_func(&mut store, "free")?; let resolve_url: TypedFunc<(u32, u32, u32, u32, u32, u32), u32> = instance.get_typed_func(&mut store, "resolve_url")?; - let first = call(&mut store, memory, &alloc, &free, &resolve_url, spec)?; + let first = call_resolve(&mut store, memory, &alloc, &free, &resolve_url, spec)?; let mut samples = Vec::with_capacity(iterations); for _ in 0..iterations { let start = Instant::now(); - let _ = call(&mut store, memory, &alloc, &free, &resolve_url, spec)?; + let _ = call_resolve(&mut store, memory, &alloc, &free, &resolve_url, spec)?; samples.push(start.elapsed()); } let (median, p95) = stats(samples); + Ok(ResolveResult { + output: first, + median, + p95, + }) + } + + pub fn run_render(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { + let engine = Engine::default(); + let module = Module::new(&engine, wasm_bytes)?; + let state = HostState::new()?; + let linker = build_linker::(&engine)?; + let mut store = Store::new(&engine, state); + let instance = linker.instantiate(&mut store, &module)?; + + let memory = instance + .get_memory(&mut store, "memory") + .context("wasm module is missing the `memory` export")?; + let alloc: TypedFunc = instance.get_typed_func(&mut store, "alloc")?; + let free: TypedFunc<(u32, u32), ()> = instance.get_typed_func(&mut store, "free")?; + let render_spec: TypedFunc<(u32, u32, u32, u32, u32, u32), i32> = instance + .get_typed_func(&mut store, "render_spec") + .map_err(|e| { + anyhow::anyhow!("artifact marked full but does not export render_spec: {e}") + })?; + + // Stash hot handles on the Store data so `fetch_bytes` can call + // `alloc` reentrantly. + store.data_mut().memory = Some(memory); + store.data_mut().alloc = Some(alloc.clone()); + + let mut fetch_samples = Vec::with_capacity(iterations); + let mut render_samples = Vec::with_capacity(iterations); + let mut total_samples = Vec::with_capacity(iterations); + let mut last_output: Option = None; + + for _ in 0..iterations { + store.data_mut().last_fetch = None; + let total_start = Instant::now(); + let out = call_render(&mut store, memory, &alloc, &free, &render_spec, spec)?; + let total = total_start.elapsed(); + + let fetch = store.data().last_fetch.unwrap_or(Duration::ZERO); + let render = total.saturating_sub(fetch); + fetch_samples.push(fetch); + render_samples.push(render); + total_samples.push(total); + last_output = Some(out); + } + + let output = last_output.context("render_spec produced no output")?; + Ok(RenderResult { + output, + fetch_median: median_duration(fetch_samples), + render_median: median_duration(render_samples), + total_median: median_duration(total_samples), + }) + } + + struct HostState { + client: reqwest::blocking::Client, + cache: Arc>>>, + last_fetch: Option, + memory: Option, + alloc: Option>, + } + + impl HostState { + fn new() -> Result { + Ok(Self { + client: blocking_http_client()?, + cache: Arc::new(Mutex::new(HashMap::new())), + last_fetch: None, + memory: None, + alloc: None, + }) + } + } + + trait MaybeHostState { + fn host(&mut self) -> Option<&mut HostState>; + } + impl MaybeHostState for () { + fn host(&mut self) -> Option<&mut HostState> { + None + } + } + impl MaybeHostState for HostState { + fn host(&mut self) -> Option<&mut HostState> { + Some(self) + } + } + + fn build_linker(engine: &Engine) -> Result> { + let mut linker: Linker = Linker::new(engine); + linker.func_wrap( + "env", + "fetch_bytes", + |mut caller: Caller<'_, T>, + url_ptr: u32, + url_len: u32, + buf_ptr_out: u32, + buf_len_out: u32| + -> i32 { + fetch_bytes_impl(&mut caller, url_ptr, url_len, buf_ptr_out, buf_len_out) + .unwrap_or(-1) + }, + )?; + Ok(linker) + } + + fn fetch_bytes_impl( + caller: &mut Caller<'_, T>, + url_ptr: u32, + url_len: u32, + buf_ptr_out: u32, + buf_len_out: u32, + ) -> Result { + let memory = caller.data_mut().host().and_then(|s| s.memory).context( + "fetch_bytes invoked without a HostState (minimal artifact should not call it)", + )?; + let alloc_fn = caller + .data_mut() + .host() + .and_then(|s| s.alloc.clone()) + .context("alloc handle missing from HostState")?; + + let mut url_bytes = vec![0u8; url_len as usize]; + memory.read(&*caller, url_ptr as usize, &mut url_bytes)?; + let url = String::from_utf8(url_bytes).context("fetch_bytes: url not utf-8")?; + + let start = Instant::now(); + let body = { + let cache = caller + .data_mut() + .host() + .map(|s| Arc::clone(&s.cache)) + .expect("host state"); + let guard = cache.lock().unwrap(); + if let Some(cached) = guard.get(&url) { + cached.clone() + } else { + let client = caller + .data_mut() + .host() + .map(|s| s.client.clone()) + .expect("host state"); + drop(guard); + let resp = client.get(&url).send().context("fetch_bytes: GET failed")?; + let status = resp.status(); + if !status.is_success() { + return Ok(status.as_u16() as i32); + } + let bytes = resp.bytes().context("fetch_bytes: read body failed")?; + let vec = bytes.to_vec(); + cache.lock().unwrap().insert(url.clone(), vec.clone()); + vec + } + }; + if let Some(state) = caller.data_mut().host() { + state.last_fetch = Some(start.elapsed()); + } - Ok(RunResult { output: first, median, p95 }) + let buf_ptr = alloc_fn.call(&mut *caller, body.len() as u32)?; + if buf_ptr == 0 { + return Ok(-1); + } + memory.write(&mut *caller, buf_ptr as usize, &body)?; + memory.write(&mut *caller, buf_ptr_out as usize, &buf_ptr.to_le_bytes())?; + memory.write( + &mut *caller, + buf_len_out as usize, + &(body.len() as u32).to_le_bytes(), + )?; + Ok(0) } - fn call( - store: &mut Store<()>, + fn call_resolve( + store: &mut Store, memory: Memory, alloc: &TypedFunc, free: &TypedFunc<(u32, u32), ()>, @@ -275,7 +643,6 @@ mod wasmtime_runner { &mut *store, (spec_ptr, spec_len, target_ptr, target_len, out_ptr, OUT_CAP), )?; - let output = if n == 0 { None } else { @@ -289,17 +656,96 @@ mod wasmtime_runner { free.call(&mut *store, (target_ptr, target_len))?; } free.call(&mut *store, (out_ptr, OUT_CAP))?; - Ok(output) } + + fn call_render( + store: &mut Store, + memory: Memory, + alloc: &TypedFunc, + free: &TypedFunc<(u32, u32), ()>, + render_spec: &TypedFunc<(u32, u32, u32, u32, u32, u32), i32>, + spec: &Spec, + ) -> Result { + let spec_len = spec.spec.len() as u32; + let spec_ptr = alloc.call(&mut *store, spec_len)?; + if spec_ptr == 0 { + bail!("alloc(spec) returned null"); + } + memory.write(&mut *store, spec_ptr as usize, spec.spec.as_bytes())?; + + let (target_ptr, target_len) = if let Some(t) = spec.target { + let p = alloc.call(&mut *store, t.len() as u32)?; + if p == 0 { + bail!("alloc(target) returned null"); + } + memory.write(&mut *store, p as usize, t.as_bytes())?; + (p, t.len() as u32) + } else { + (0, 0) + }; + + // Reserve two u32 slots to receive the output ptr/len. + let slot_ptr = alloc.call(&mut *store, 8)?; + if slot_ptr == 0 { + bail!("alloc(slots) returned null"); + } + memory.write(&mut *store, slot_ptr as usize, &[0u8; 8])?; + + let rc = render_spec.call( + &mut *store, + ( + spec_ptr, + spec_len, + target_ptr, + target_len, + slot_ptr, + slot_ptr + 4, + ), + )?; + if rc != 0 { + bail!("render_spec failed with code {rc}"); + } + + let mut slots = [0u8; 8]; + memory.read(&*store, slot_ptr as usize, &mut slots)?; + let out_ptr = u32::from_le_bytes(slots[0..4].try_into().unwrap()); + let out_len = u32::from_le_bytes(slots[4..8].try_into().unwrap()); + let mut buf = vec![0u8; out_len as usize]; + memory.read(&*store, out_ptr as usize, &mut buf)?; + let md = String::from_utf8(buf).context("render_spec returned non-UTF8 bytes")?; + + free.call(&mut *store, (spec_ptr, spec_len))?; + if target_len > 0 { + free.call(&mut *store, (target_ptr, target_len))?; + } + free.call(&mut *store, (slot_ptr, 8))?; + free.call(&mut *store, (out_ptr, out_len))?; + + Ok(md) + } } #[cfg(feature = "wasmer")] mod wasmer_runner { use super::*; - use wasmer::{Instance, Memory, Module, Store, TypedFunction, imports}; + use std::sync::{Arc, Mutex}; + use std::time::Instant; + use wasmer::{ + AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Instance, Memory, Module, Store, + TypedFunction, imports, + }; + + #[derive(Clone)] + struct HostEnv { + client: reqwest::blocking::Client, + cache: Arc>>>, + last_fetch: Arc>>, + memory: Arc>>, + alloc: Arc>>>, + } - pub fn run(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { + pub fn run_resolve(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { let mut store = Store::default(); let module = Module::new(&store, wasm_bytes)?; let instance = Instance::new(&mut store, &module, &imports! {})?; @@ -311,20 +757,154 @@ mod wasmer_runner { let resolve_url: TypedFunction<(u32, u32, u32, u32, u32, u32), u32> = instance.exports.get_typed_function(&store, "resolve_url")?; - let first = call(&mut store, &memory, &alloc, &free, &resolve_url, spec)?; + let first = call_resolve(&mut store, &memory, &alloc, &free, &resolve_url, spec)?; let mut samples = Vec::with_capacity(iterations); for _ in 0..iterations { let start = Instant::now(); - let _ = call(&mut store, &memory, &alloc, &free, &resolve_url, spec)?; + let _ = call_resolve(&mut store, &memory, &alloc, &free, &resolve_url, spec)?; samples.push(start.elapsed()); } let (median, p95) = stats(samples); + Ok(ResolveResult { + output: first, + median, + p95, + }) + } + + pub fn run_render(wasm_bytes: &[u8], spec: &Spec, iterations: usize) -> Result { + let mut store = Store::default(); + let module = Module::new(&store, wasm_bytes)?; + + let env = HostEnv { + client: blocking_http_client()?, + cache: Arc::new(Mutex::new(HashMap::new())), + last_fetch: Arc::new(Mutex::new(None)), + memory: Arc::new(Mutex::new(None)), + alloc: Arc::new(Mutex::new(None)), + }; + let fn_env = FunctionEnv::new(&mut store, env.clone()); + + let fetch_bytes = Function::new_typed_with_env( + &mut store, + &fn_env, + |mut caller: FunctionEnvMut, + url_ptr: u32, + url_len: u32, + buf_ptr_out: u32, + buf_len_out: u32| + -> i32 { + fetch_bytes_impl(&mut caller, url_ptr, url_len, buf_ptr_out, buf_len_out) + .unwrap_or(-1) + }, + ); + + let imports = imports! { + "env" => { "fetch_bytes" => fetch_bytes }, + }; + let instance = Instance::new(&mut store, &module, &imports)?; + let memory = instance.exports.get_memory("memory")?.clone(); + let alloc: TypedFunction = + instance.exports.get_typed_function(&store, "alloc")?; + let free: TypedFunction<(u32, u32), ()> = + instance.exports.get_typed_function(&store, "free")?; + let render_spec: TypedFunction<(u32, u32, u32, u32, u32, u32), i32> = instance + .exports + .get_typed_function(&store, "render_spec") + .context("artifact marked full but does not export render_spec")?; + + *env.memory.lock().unwrap() = Some(memory.clone()); + *env.alloc.lock().unwrap() = Some(alloc.clone()); + + let mut fetch_samples = Vec::with_capacity(iterations); + let mut render_samples = Vec::with_capacity(iterations); + let mut total_samples = Vec::with_capacity(iterations); + let mut last_output: Option = None; - Ok(RunResult { output: first, median, p95 }) + for _ in 0..iterations { + *env.last_fetch.lock().unwrap() = None; + let total_start = Instant::now(); + let out = call_render(&mut store, &memory, &alloc, &free, &render_spec, spec)?; + let total = total_start.elapsed(); + let fetch = env.last_fetch.lock().unwrap().unwrap_or(Duration::ZERO); + let render = total.saturating_sub(fetch); + fetch_samples.push(fetch); + render_samples.push(render); + total_samples.push(total); + last_output = Some(out); + } + + let output = last_output.context("render_spec produced no output")?; + Ok(RenderResult { + output, + fetch_median: median_duration(fetch_samples), + render_median: median_duration(render_samples), + total_median: median_duration(total_samples), + }) } - fn call( + fn fetch_bytes_impl( + caller: &mut FunctionEnvMut, + url_ptr: u32, + url_len: u32, + buf_ptr_out: u32, + buf_len_out: u32, + ) -> Result { + let (memory, alloc_fn) = { + let env = caller.data(); + ( + env.memory + .lock() + .unwrap() + .clone() + .context("memory not set")?, + env.alloc.lock().unwrap().clone().context("alloc not set")?, + ) + }; + + let view = memory.view(&*caller); + let mut url_bytes = vec![0u8; url_len as usize]; + view.read(url_ptr as u64, &mut url_bytes)?; + let url = String::from_utf8(url_bytes).context("fetch_bytes: url not utf-8")?; + + let start = Instant::now(); + let body = { + let (cache, client) = { + let env = caller.data(); + (Arc::clone(&env.cache), env.client.clone()) + }; + let cached = cache.lock().unwrap().get(&url).cloned(); + if let Some(v) = cached { + v + } else { + let resp = client.get(&url).send().context("fetch_bytes: GET failed")?; + let status = resp.status(); + if !status.is_success() { + return Ok(status.as_u16() as i32); + } + let bytes = resp + .bytes() + .context("fetch_bytes: read body failed")? + .to_vec(); + cache.lock().unwrap().insert(url.clone(), bytes.clone()); + bytes + } + }; + *caller.data().last_fetch.lock().unwrap() = Some(start.elapsed()); + + let buf_ptr = alloc_fn.call(&mut caller.as_store_mut(), body.len() as u32)?; + if buf_ptr == 0 { + return Ok(-1); + } + let view = memory.view(&*caller); + view.write(buf_ptr as u64, &body)?; + view.write(buf_ptr_out as u64, &buf_ptr.to_le_bytes())?; + view.write(buf_len_out as u64, &(body.len() as u32).to_le_bytes())?; + Ok(0) + } + + fn call_resolve( store: &mut Store, memory: &Memory, alloc: &TypedFunction, @@ -366,7 +946,6 @@ mod wasmer_runner { out_ptr, OUT_CAP, )?; - let output = if n == 0 { None } else { @@ -380,7 +959,70 @@ mod wasmer_runner { free.call(&mut *store, target_ptr, target_len)?; } free.call(&mut *store, out_ptr, OUT_CAP)?; - Ok(output) } + + fn call_render( + store: &mut Store, + memory: &Memory, + alloc: &TypedFunction, + free: &TypedFunction<(u32, u32), ()>, + render_spec: &TypedFunction<(u32, u32, u32, u32, u32, u32), i32>, + spec: &Spec, + ) -> Result { + let spec_len = spec.spec.len() as u32; + let spec_ptr = alloc.call(&mut *store, spec_len)?; + if spec_ptr == 0 { + bail!("alloc(spec) returned null"); + } + memory + .view(&*store) + .write(spec_ptr as u64, spec.spec.as_bytes())?; + + let (target_ptr, target_len) = if let Some(t) = spec.target { + let p = alloc.call(&mut *store, t.len() as u32)?; + if p == 0 { + bail!("alloc(target) returned null"); + } + memory.view(&*store).write(p as u64, t.as_bytes())?; + (p, t.len() as u32) + } else { + (0, 0) + }; + + let slot_ptr = alloc.call(&mut *store, 8)?; + if slot_ptr == 0 { + bail!("alloc(slots) returned null"); + } + memory.view(&*store).write(slot_ptr as u64, &[0u8; 8])?; + + let rc = render_spec.call( + &mut *store, + spec_ptr, + spec_len, + target_ptr, + target_len, + slot_ptr, + slot_ptr + 4, + )?; + if rc != 0 { + bail!("render_spec failed with code {rc}"); + } + + let mut slots = [0u8; 8]; + memory.view(&*store).read(slot_ptr as u64, &mut slots)?; + let out_ptr = u32::from_le_bytes(slots[0..4].try_into().unwrap()); + let out_len = u32::from_le_bytes(slots[4..8].try_into().unwrap()); + let mut buf = vec![0u8; out_len as usize]; + memory.view(&*store).read(out_ptr as u64, &mut buf)?; + let md = String::from_utf8(buf).context("render_spec returned non-UTF8 bytes")?; + + free.call(&mut *store, spec_ptr, spec_len)?; + if target_len > 0 { + free.call(&mut *store, target_ptr, target_len)?; + } + free.call(&mut *store, slot_ptr, 8)?; + free.call(&mut *store, out_ptr, out_len)?; + Ok(md) + } }