@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 +28 -0
- package/build.zig +1 -1
- package/package.json +2 -2
- package/src/char_utils.zig +78 -12
- package/src/emitter.zig +324 -179
- package/src/extractors.zig +724 -404
- package/src/lib.zig +35 -8
- package/src/main.zig +108 -77
- package/src/scan_loop.zig +101 -65
- package/src/scanner.zig +293 -106
- package/src/type_inference.zig +215 -130
- package/test/zig-dtsx.test.ts +5 -1
- package/zig-out/bin/zig-dtsx +0 -0
- package/zig-out/bin/zig-dtsx.exe +0 -0
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
|
|
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.
|
|
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.
|
|
21
|
+
"@stacksjs/dtsx": "0.9.14"
|
|
22
22
|
}
|
|
23
23
|
}
|
package/src/char_utils.zig
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
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));
|