@stacksjs/zig-dtsx 0.9.13 → 0.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,6 +52,34 @@ The Zig binary communicates with the Node/Bun runtime via FFI, allowing seamless
52
52
 
53
53
  ## Benchmarks
54
54
 
55
+ Benchmarked on Apple M3 Pro, macOS _(bun 1.3.11, arm64-darwin)_.
56
+
57
+ ### In-Process API — No Cache
58
+
59
+ | Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
60
+ |------|-------------------|---------------------|--------------------|--------------------|
61
+ | **zig-dtsx**|**3.37 µs**|**7.05 µs**|**21.89 µs**|**144.89 µs** |
62
+ | oxc-transform | 7.36 µs _(2.2x)_ | 21.91 µs _(3.1x)_ | 89.66 µs _(4.1x)_ | 560.86 µs _(3.9x)_ |
63
+ | tsc | 169.69 µs _(50.4x)_ | 410.31 µs _(58.2x)_ | 1.03 ms _(47.1x)_ | 4.02 ms _(27.7x)_ |
64
+
65
+ ### CLI — Single File
66
+
67
+ | Tool | Small (~50 lines) | Medium (~100 lines) | Large (~330 lines) | XLarge (~1050 lines) |
68
+ |------|-------------------|---------------------|--------------------|--------------------|
69
+ | **zig-dtsx**|**2.69 ms**|**2.35 ms**|**2.28 ms**|**3.14 ms** |
70
+ | oxc | 17.08 ms _(6.3x)_ | 17.12 ms _(7.3x)_ | 17.95 ms _(7.9x)_ | 17.69 ms _(5.6x)_ |
71
+ | tsgo | 40.53 ms _(15.1x)_ | 44.10 ms _(18.8x)_ | 44.39 ms _(19.5x)_ | 57.77 ms _(18.4x)_ |
72
+ | tsc | 384.25 ms _(142.8x)_ | 407.51 ms _(173.4x)_ | 418.81 ms _(183.7x)_ | 454.74 ms _(144.8x)_ |
73
+
74
+ ### Multi-File Project
75
+
76
+ | Tool | 50 files | 100 files | 500 files |
77
+ |------|----------|-----------|-----------|
78
+ | **zig-dtsx**|**18.10 ms**|**31.46 ms**|**~140 ms** |
79
+ | oxc | 48.27 ms _(2.7x)_ | 79.00 ms _(2.5x)_ | ~365 ms _(2.6x)_ |
80
+ | tsgo | 244.68 ms _(13.5x)_ | 419.65 ms _(13.3x)_ | - |
81
+ | tsc | 871.48 ms _(48.1x)_ | - | - |
82
+
55
83
  ```bash
56
84
  bun run benchmark
57
85
  ```
package/build.zig CHANGED
@@ -15,7 +15,7 @@ pub fn build(b: *std.Build) void {
15
15
  .link_libc = true,
16
16
  }),
17
17
  });
18
- // Enable LTO on Linux release builds (LLD required; macOS Mach-O doesn't support LLD)
18
+ // Enable LTO on Linux release builds (requires LLD; macOS Mach-O doesn't support LLD yet)
19
19
  if (target.result.os.tag == .linux and optimize != .Debug) {
20
20
  lib.use_lld = true;
21
21
  lib.lto = .full;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/zig-dtsx",
3
3
  "type": "module",
4
- "version": "0.9.13",
4
+ "version": "0.9.14",
5
5
  "description": "High-performance DTS emitter written in Zig",
6
6
  "exports": {
7
7
  ".": {
@@ -18,6 +18,6 @@
18
18
  "benchmark": "bun run test/benchmark.ts"
19
19
  },
20
20
  "dependencies": {
21
- "@stacksjs/dtsx": "0.9.13"
21
+ "@stacksjs/dtsx": "0.9.14"
22
22
  }
23
23
  }
@@ -61,30 +61,79 @@ pub inline fn isDigit(ch: u8) bool {
61
61
  return ch >= '0' and ch <= '9';
62
62
  }
63
63
 
64
- /// Find needle in haystack starting from start position
64
+ /// Find needle in haystack starting from start position.
65
+ /// For single-byte needles, uses SIMD. For multi-byte, uses SIMD first-byte scan + verify.
65
66
  pub inline fn indexOf(haystack: []const u8, needle: []const u8, start: usize) ?usize {
66
67
  if (needle.len == 0) return start;
67
68
  if (start + needle.len > haystack.len) return null;
68
- return std.mem.indexOfPos(u8, haystack, start, needle);
69
+ if (needle.len == 1) return indexOfChar(haystack, needle[0], start);
70
+ // SIMD first-byte scan: find positions where needle[0] matches, then verify rest
71
+ const first = needle[0];
72
+ const search = haystack[start..];
73
+ var i: usize = 0;
74
+ while (i + needle.len <= search.len) {
75
+ // SIMD: scan for first byte of needle
76
+ while (i + 16 <= search.len) {
77
+ const chunk: @Vector(16, u8) = search[i..][0..16].*;
78
+ const match_mask = chunk == @as(@Vector(16, u8), @splat(first));
79
+ if (@reduce(.Or, match_mask)) {
80
+ const bits: u16 = @bitCast(match_mask);
81
+ const offset = @ctz(bits);
82
+ i += offset;
83
+ break;
84
+ }
85
+ i += 16;
86
+ }
87
+ if (i + needle.len > search.len) return null;
88
+ // Verify full needle match
89
+ if (search[i] == first and std.mem.eql(u8, search[i..][0..needle.len], needle)) {
90
+ return start + i;
91
+ }
92
+ i += 1;
93
+ }
94
+ return null;
69
95
  }
70
96
 
71
- /// Find a single byte in haystack starting from start position
97
+ /// Find a single byte in haystack starting from start position.
98
+ /// Uses SIMD to scan 16 bytes at a time for the target character.
72
99
  pub inline fn indexOfChar(haystack: []const u8, needle: u8, start: usize) ?usize {
73
100
  if (start >= haystack.len) return null;
74
- return std.mem.indexOfScalarPos(u8, haystack, start, needle);
101
+ const search = haystack[start..];
102
+ // SIMD fast path: scan 16 bytes at a time
103
+ var i: usize = 0;
104
+ while (i + 16 <= search.len) {
105
+ const chunk: @Vector(16, u8) = search[i..][0..16].*;
106
+ const match_mask = chunk == @as(@Vector(16, u8), @splat(needle));
107
+ if (@reduce(.Or, match_mask)) {
108
+ const bits: u16 = @bitCast(match_mask);
109
+ return start + i + @ctz(bits);
110
+ }
111
+ i += 16;
112
+ }
113
+ // Scalar fallback for remaining bytes
114
+ while (i < search.len) : (i += 1) {
115
+ if (search[i] == needle) return start + i;
116
+ }
117
+ return null;
75
118
  }
76
119
 
77
120
  /// Slice source[start..end) with leading/trailing whitespace trimmed
78
- pub fn sliceTrimmed(source: []const u8, start_pos: usize, end_pos: usize) []const u8 {
79
- var s = start_pos;
80
- var e = end_pos;
81
- if (s >= e) return "";
82
-
83
- // Fast path: if endpoints are already non-whitespace, skip trim loops
84
- if (!isWhitespace(source[s]) and !isWhitespace(source[e - 1])) {
85
- return source[s..e];
121
+ pub inline fn sliceTrimmed(source: []const u8, start_pos: usize, end_pos: usize) []const u8 {
122
+ if (start_pos >= end_pos) return "";
123
+
124
+ // Fast path: if endpoints are already non-whitespace, skip trim loops.
125
+ // Most slices in TS source already have clean boundaries — this branch
126
+ // dominates and is worth keeping out of any function call.
127
+ const first = source[start_pos];
128
+ const last = source[end_pos - 1];
129
+ if (first != CH_SPACE and first != CH_TAB and first != CH_LF and first != CH_CR and
130
+ last != CH_SPACE and last != CH_TAB and last != CH_LF and last != CH_CR)
131
+ {
132
+ return source[start_pos..end_pos];
86
133
  }
87
134
 
135
+ var s = start_pos;
136
+ var e = end_pos;
88
137
  while (s < e and isWhitespace(source[s])) s += 1;
89
138
  while (e > s and isWhitespace(source[e - 1])) e -= 1;
90
139
  return source[s..e];
@@ -143,6 +192,23 @@ test "sliceTrimmed" {
143
192
  try std.testing.expectEqualStrings("hello", sliceTrimmed(src, 2, 7));
144
193
  }
145
194
 
195
+ test "sliceTrimmed fast path: clean endpoints return zero-copy slice" {
196
+ // After the inline + manual-whitespace-check rewrite the fast path must
197
+ // still bypass the trim loops when the endpoints are non-whitespace.
198
+ const src = "abc def";
199
+ const out = sliceTrimmed(src, 0, src.len);
200
+ try std.testing.expectEqualStrings("abc def", out);
201
+ // Returned slice points into the same buffer (no copy).
202
+ try std.testing.expect(out.ptr == src.ptr);
203
+ }
204
+
205
+ test "sliceTrimmed handles tabs, newlines, and CR" {
206
+ try std.testing.expectEqualStrings("x", sliceTrimmed("\tx\n", 0, 3));
207
+ try std.testing.expectEqualStrings("x", sliceTrimmed("\rx\r", 0, 3));
208
+ try std.testing.expectEqualStrings("", sliceTrimmed(" ", 0, 3));
209
+ try std.testing.expectEqualStrings("", sliceTrimmed("abc", 1, 1));
210
+ }
211
+
146
212
  test "indexOf" {
147
213
  const s = "hello world";
148
214
  try std.testing.expectEqual(@as(?usize, 6), indexOf(s, "world", 0));