@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/src/emitter.zig CHANGED
@@ -12,29 +12,40 @@ inline fn isIdentChar(c: u8) bool {
12
12
  return (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_' or c == '$' or c > 127;
13
13
  }
14
14
 
15
- /// Check if `name` appears as a whole word in `text` (fast indexOf + boundary check)
16
- pub fn isWordInText(name: []const u8, text: []const u8) bool {
17
- if (name.len == 0) return false;
18
- var search_from: usize = 0;
19
- while (search_from < text.len) {
20
- const idx = ch.indexOf(text, name, search_from) orelse return false;
21
- const before: u8 = if (idx > 0) text[idx - 1] else ' ';
22
- const after: u8 = if (idx + name.len < text.len) text[idx + name.len] else ' ';
23
- if (!isIdentChar(before) and !isIdentChar(after)) return true;
24
- search_from = idx + 1;
25
- }
26
- return false;
27
- }
15
+ /// Check if `name` appears as a whole word in `text` — delegated to scanner.zig (single source of truth)
16
+ pub const isWordInText = @import("scanner.zig").isWordInText;
28
17
 
29
- /// Extract all identifier words from text into an existing HashMap (single pass, O(n))
18
+ /// Extract all identifier words from text into an existing HashMap (single pass, O(n)).
19
+ /// Uses SIMD to skip 16 non-identifier bytes at a time.
30
20
  fn extractWords(words: *std.StringHashMap(void), text: []const u8) void {
31
21
  var i: usize = 0;
32
22
  while (i < text.len) {
23
+ // SIMD fast-skip: tighten the "interesting byte" predicate to actual
24
+ // identifier-start chars. Previously the `>= 'A'` check accepted brace
25
+ // and bracket characters too (all in 91..96, 123..126), causing
26
+ // unnecessary scalar fall-throughs for type-heavy declarations.
27
+ while (i + 16 <= text.len) {
28
+ const chunk: @Vector(16, u8) = text[i..][0..16].*;
29
+ const upper = (chunk >= @as(@Vector(16, u8), @splat('A'))) & (chunk <= @as(@Vector(16, u8), @splat('Z')));
30
+ const lower = (chunk >= @as(@Vector(16, u8), @splat('a'))) & (chunk <= @as(@Vector(16, u8), @splat('z')));
31
+ const is_dollar = chunk == @as(@Vector(16, u8), @splat('$'));
32
+ const is_under = chunk == @as(@Vector(16, u8), @splat('_'));
33
+ const is_high = chunk > @as(@Vector(16, u8), @splat(127));
34
+ const interesting = upper | lower | is_dollar | is_under | is_high;
35
+ if (@reduce(.Or, interesting)) {
36
+ const bits: u16 = @bitCast(interesting);
37
+ i += @ctz(bits);
38
+ break;
39
+ }
40
+ i += 16;
41
+ }
42
+ if (i >= text.len) break;
43
+
33
44
  const c = text[i];
34
- if ((c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or c == '_' or c == '$' or c > 127) {
45
+ if (ch.isIdentStart(c)) {
35
46
  const start = i;
36
47
  i += 1;
37
- while (i < text.len and isIdentChar(text[i])) i += 1;
48
+ while (i < text.len and ch.isIdentChar(text[i])) i += 1;
38
49
  words.put(text[start..i], {}) catch {};
39
50
  } else {
40
51
  i += 1;
@@ -45,6 +56,8 @@ fn extractWords(words: *std.StringHashMap(void), text: []const u8) void {
45
56
  /// Extract triple-slash directives from source start
46
57
  fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![][]const u8 {
47
58
  var directives = std.array_list.Managed([]const u8).init(alloc);
59
+ // Pre-allocate small capacity — typical files have 0-3 directives.
60
+ try directives.ensureTotalCapacity(2);
48
61
  var line_start: usize = 0;
49
62
 
50
63
  var i: usize = 0;
@@ -59,8 +72,14 @@ fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![
59
72
  line_start = i + 1;
60
73
 
61
74
  if (trimmed.len >= 3 and trimmed[0] == '/' and trimmed[1] == '/' and trimmed[2] == '/') {
62
- // Check for /// <reference .../>
63
- if (ch.contains(trimmed, "<reference ") or ch.contains(trimmed, "<amd-module ") or ch.contains(trimmed, "<amd-dependency ")) {
75
+ // Inline char-by-char check: avoids three full ch.contains scans.
76
+ // Look for "<reference ", "<amd-module ", "<amd-dependency ".
77
+ const lt = ch.indexOfChar(trimmed, '<', 3) orelse continue;
78
+ const tail = trimmed[lt..];
79
+ if ((tail.len > 11 and std.mem.eql(u8, tail[0..11], "<reference ")) or
80
+ (tail.len > 12 and std.mem.eql(u8, tail[0..12], "<amd-module ")) or
81
+ (tail.len > 16 and std.mem.eql(u8, tail[0..16], "<amd-dependency ")))
82
+ {
64
83
  try directives.append(trimmed);
65
84
  }
66
85
  } else if (trimmed.len == 0 or (trimmed.len >= 2 and trimmed[0] == '/' and trimmed[1] == '/')) {
@@ -74,7 +93,7 @@ fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![
74
93
  return directives.toOwnedSlice();
75
94
  }
76
95
 
77
- /// Format leading comments — direct alloc, output total comment length + newlines
96
+ /// Format leading comments — avoids allocation for the most common case (single comment).
78
97
  fn formatComments(alloc: std.mem.Allocator, comments: ?[]const []const u8, keep_comments: bool) ![]const u8 {
79
98
  if (!keep_comments) return "";
80
99
  const cmts = comments orelse return "";
@@ -82,28 +101,49 @@ fn formatComments(alloc: std.mem.Allocator, comments: ?[]const []const u8, keep_
82
101
 
83
102
  // Fast path: single comment (very common)
84
103
  if (cmts.len == 1) {
85
- const t = ch.sliceTrimmed(cmts[0], 0, cmts[0].len);
104
+ const raw = cmts[0];
105
+ const t = ch.sliceTrimmed(raw, 0, raw.len);
106
+ // Zero-alloc fast path: if the trimmed comment is the full slice AND
107
+ // there's a '\n' right after it in memory, return a slice that includes
108
+ // the newline. This works because comments point into the source buffer.
109
+ if (t.len == raw.len and t.len > 0) {
110
+ // Check if the byte after the comment in the source buffer is '\n'
111
+ const end_ptr = t.ptr + t.len;
112
+ if (@intFromPtr(end_ptr) > 0 and end_ptr[0] == '\n') {
113
+ return t.ptr[0 .. t.len + 1];
114
+ }
115
+ }
86
116
  const buf = try alloc.alloc(u8, t.len + 1);
87
117
  @memcpy(buf[0..t.len], t);
88
118
  buf[t.len] = '\n';
89
119
  return buf;
90
120
  }
91
121
 
92
- // Pre-compute exact size
122
+ // Two-pass approach: cache trimmed slices in a small stack-allocated array
123
+ // when the count is reasonable, or a heap-allocated array otherwise. This
124
+ // avoids running sliceTrimmed twice for each comment.
125
+ var inline_buf: [8][]const u8 = undefined;
126
+ const trimmed_slices = if (cmts.len <= inline_buf.len)
127
+ inline_buf[0..cmts.len]
128
+ else
129
+ try alloc.alloc([]const u8, cmts.len);
130
+ defer if (cmts.len > inline_buf.len) alloc.free(trimmed_slices);
131
+
93
132
  var total_len: usize = cmts.len; // newlines between + trailing
94
- for (cmts) |c| {
133
+ for (cmts, 0..) |c, i| {
95
134
  const t = ch.sliceTrimmed(c, 0, c.len);
135
+ trimmed_slices[i] = t;
96
136
  total_len += t.len;
97
137
  }
98
138
 
99
139
  const buf = try alloc.alloc(u8, total_len);
100
- var pos: usize = 0;
101
- for (cmts, 0..) |c, idx| {
102
- if (idx > 0) {
103
- buf[pos] = '\n';
104
- pos += 1;
105
- }
106
- const t = ch.sliceTrimmed(c, 0, c.len);
140
+ // First slice write without leading newline.
141
+ @memcpy(buf[0..trimmed_slices[0].len], trimmed_slices[0]);
142
+ var pos: usize = trimmed_slices[0].len;
143
+ // Subsequent slices — newline-separated.
144
+ for (trimmed_slices[1..]) |t| {
145
+ buf[pos] = '\n';
146
+ pos += 1;
107
147
  @memcpy(buf[pos..][0..t.len], t);
108
148
  pos += t.len;
109
149
  }
@@ -156,10 +196,13 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
156
196
  // The scanner already built the correct DTS text using the type annotation,
157
197
  // so we can return it directly without type inference or value processing.
158
198
  if (decl.type_annotation.len > 0 and decl.value.len > 0) {
159
- const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
160
- if (!ch.endsWith(trimmed_val, "as const") and
161
- !ch.contains(decl.value, " satisfies "))
162
- {
199
+ // Cheaper "ends with as const" check that skips the trim allocation.
200
+ // We just need to ignore trailing whitespace.
201
+ var ve: usize = decl.value.len;
202
+ while (ve > 0 and (decl.value[ve - 1] == ' ' or decl.value[ve - 1] == '\t' or
203
+ decl.value[ve - 1] == '\n' or decl.value[ve - 1] == '\r')) : (ve -= 1) {}
204
+ const ends_as_const = ve >= 8 and std.mem.eql(u8, decl.value[ve - 8 .. ve], "as const");
205
+ if (!ends_as_const and !ch.contains(decl.value, " satisfies ")) {
163
206
  const kind: []const u8 = if (decl.modifiers) |mods| (if (mods.len > 0) mods[0] else "const") else "const";
164
207
  if (!std.mem.eql(u8, kind, "const") or !type_inf.isGenericType(decl.type_annotation)) {
165
208
  if (comments.len == 0) return decl.text;
@@ -200,18 +243,24 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
200
243
  if (type_annotation.len == 0) type_annotation = "unknown";
201
244
 
202
245
  // Add @defaultValue JSDoc for widened declarations
203
- // Skip when value uses 'as const' — types are already narrow/self-documenting
246
+ // Skip when value uses 'as const' — types are already narrow/self-documenting.
247
+ // Cache the trimmed value so we don't trim a second time below.
204
248
  const val_trimmed_for_check = std.mem.trim(u8, decl.value, " \t\n\r");
205
249
  const has_as_const = ch.endsWith(val_trimmed_for_check, "as const");
206
250
  if (decl.value.len > 0 and decl.type_annotation.len == 0 and !has_as_const) {
207
251
  var default_val: ?[]const u8 = null;
208
252
  var is_container = false;
209
- const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
253
+ const trimmed_val = val_trimmed_for_check;
210
254
 
211
255
  if (!std.mem.eql(u8, kind, "const")) {
212
- const is_widened = std.mem.eql(u8, type_annotation, "string") or
213
- std.mem.eql(u8, type_annotation, "number") or
214
- std.mem.eql(u8, type_annotation, "boolean");
256
+ // Length-first dispatch three primitive types have distinct lengths
257
+ // (string=6, number=6, boolean=7) so we can short-circuit before
258
+ // running std.mem.eql on a non-matching annotation.
259
+ const is_widened = switch (type_annotation.len) {
260
+ 6 => std.mem.eql(u8, type_annotation, "string") or std.mem.eql(u8, type_annotation, "number"),
261
+ 7 => std.mem.eql(u8, type_annotation, "boolean"),
262
+ else => false,
263
+ };
215
264
  if (is_widened and trimmed_val.len > 0) {
216
265
  default_val = trimmed_val;
217
266
  }
@@ -231,7 +280,7 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
231
280
  const tag_mem = try alloc.alloc(u8, tag_max);
232
281
  var tp: usize = 0;
233
282
 
234
- if (std.mem.indexOf(u8, dv, "\n")) |_| {
283
+ if (std.mem.indexOfScalar(u8, dv, '\n')) |_| {
235
284
  const hdr = "@defaultValue\n * ```ts\n";
236
285
  @memcpy(tag_mem[tp..][0..hdr.len], hdr); tp += hdr.len;
237
286
  var line_iter = std.mem.splitScalar(u8, dv, '\n');
@@ -265,7 +314,7 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
265
314
  var end = before_raw.len;
266
315
  while (end > 0 and (before_raw[end - 1] == ' ' or before_raw[end - 1] == '\t' or before_raw[end - 1] == '\n' or before_raw[end - 1] == '\r')) : (end -= 1) {}
267
316
  const before = before_raw[0..end];
268
- if (before.len > 4 and ch.startsWith(before, "/** ") and std.mem.indexOf(u8, before, "\n") == null) {
317
+ if (before.len > 4 and ch.startsWith(before, "/** ") and std.mem.indexOfScalar(u8, before, '\n') == null) {
269
318
  @memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
270
319
  @memcpy(rbuf[rp..][0..before.len - 4], before[4..]); rp += before.len - 4;
271
320
  } else {
@@ -288,7 +337,7 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
288
337
  @memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
289
338
  @memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
290
339
  } else {
291
- if (std.mem.indexOf(u8, default_tag, "\n") != null) {
340
+ if (std.mem.indexOfScalar(u8, default_tag, '\n') != null) {
292
341
  @memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
293
342
  @memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
294
343
  @memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
@@ -496,29 +545,22 @@ fn processModuleDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_co
496
545
  (decl.name.len > 0 and (decl.name[0] == '"' or decl.name[0] == '\'' or decl.name[0] == '`'));
497
546
 
498
547
  if (is_ambient) {
499
- var result = std.array_list.Managed(u8).init(alloc);
500
- try result.ensureTotalCapacity(comments.len + decl.text.len + 32);
501
- try result.appendSlice(comments);
502
- try result.appendSlice("declare module ");
503
- try result.appendSlice(decl.name);
504
-
505
- if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| {
506
- try result.append(' ');
507
- try result.appendSlice(decl.text[brace_idx..]);
508
- } else {
509
- try result.appendSlice(" {}");
510
- }
511
- return result.toOwnedSlice();
548
+ // Direct alloc (no ArrayList overhead)
549
+ const dm = "declare module ";
550
+ const body_ambient = if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| decl.text[brace_idx..] else "{}";
551
+ const total_ambient = comments.len + dm.len + decl.name.len + 1 + body_ambient.len;
552
+ const buf_ambient = try alloc.alloc(u8, total_ambient);
553
+ var ap: usize = 0;
554
+ if (comments.len > 0) { @memcpy(buf_ambient[ap..][0..comments.len], comments); ap += comments.len; }
555
+ @memcpy(buf_ambient[ap..][0..dm.len], dm); ap += dm.len;
556
+ @memcpy(buf_ambient[ap..][0..decl.name.len], decl.name); ap += decl.name.len;
557
+ buf_ambient[ap] = ' '; ap += 1;
558
+ @memcpy(buf_ambient[ap..][0..body_ambient.len], body_ambient); ap += body_ambient.len;
559
+ return buf_ambient[0..ap];
512
560
  }
513
561
 
514
- // Regular namespace
515
- var result = std.array_list.Managed(u8).init(alloc);
516
- try result.ensureTotalCapacity(comments.len + decl.text.len + 32);
517
- try result.appendSlice(comments);
518
-
519
- if (decl.is_exported) try result.appendSlice("export ");
520
-
521
- // Check if declare is already in modifiers
562
+ // Regular namespace — direct alloc (no ArrayList overhead)
563
+ const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
522
564
  var has_declare = false;
523
565
  if (decl.modifiers) |mods| {
524
566
  for (mods) |m| {
@@ -528,19 +570,20 @@ fn processModuleDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_co
528
570
  }
529
571
  }
530
572
  }
531
- if (!has_declare) try result.appendSlice("declare ");
532
-
533
- try result.appendSlice("namespace ");
534
- try result.appendSlice(decl.name);
535
-
536
- if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| {
537
- try result.append(' ');
538
- try result.appendSlice(decl.text[brace_idx..]);
539
- } else {
540
- try result.appendSlice(" {}");
541
- }
542
-
543
- return result.toOwnedSlice();
573
+ const declare_kw: []const u8 = if (!has_declare) "declare " else "";
574
+ const body = if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| decl.text[brace_idx..] else "{}";
575
+ const ns = "namespace ";
576
+ const total = comments.len + export_prefix.len + declare_kw.len + ns.len + decl.name.len + 1 + body.len;
577
+ const buf = try alloc.alloc(u8, total);
578
+ var pos: usize = 0;
579
+ if (comments.len > 0) { @memcpy(buf[pos..][0..comments.len], comments); pos += comments.len; }
580
+ @memcpy(buf[pos..][0..export_prefix.len], export_prefix); pos += export_prefix.len;
581
+ @memcpy(buf[pos..][0..declare_kw.len], declare_kw); pos += declare_kw.len;
582
+ @memcpy(buf[pos..][0..ns.len], ns); pos += ns.len;
583
+ @memcpy(buf[pos..][0..decl.name.len], decl.name); pos += decl.name.len;
584
+ buf[pos] = ' '; pos += 1;
585
+ @memcpy(buf[pos..][0..body.len], body); pos += body.len;
586
+ return buf[0..pos];
544
587
  }
545
588
 
546
589
  /// Main entry point: process declarations array into final .d.ts output.
@@ -555,7 +598,9 @@ pub fn processDeclarations(
555
598
  import_order: []const []const u8,
556
599
  ) ![]const u8 {
557
600
  var result = std.array_list.Managed(u8).init(result_alloc);
558
- try result.ensureTotalCapacity(source_code.len);
601
+ // DTS output is typically 30-60% of source size (bodies stripped, types added).
602
+ // Bias toward 60% + 256 byte slack to avoid the final 1-2 reallocs on average files.
603
+ try result.ensureTotalCapacity((source_code.len * 6) / 10 + 256);
559
604
 
560
605
  // Extract triple-slash directives
561
606
  // Fast check: skip whitespace
@@ -569,58 +614,94 @@ pub fn processDeclarations(
569
614
  }
570
615
  }
571
616
 
572
- // Group declarations by type (pre-size to avoid incremental reallocation)
573
- const group_cap: usize = @max(declarations.len / 4, 4);
574
- var imports = std.array_list.Managed(Declaration).init(alloc);
575
- try imports.ensureTotalCapacity(group_cap);
576
- var functions = std.array_list.Managed(Declaration).init(alloc);
577
- try functions.ensureTotalCapacity(group_cap);
578
- var variables = std.array_list.Managed(Declaration).init(alloc);
579
- try variables.ensureTotalCapacity(group_cap);
580
- var interfaces = std.array_list.Managed(Declaration).init(alloc);
581
- try interfaces.ensureTotalCapacity(group_cap);
582
- var type_decls = std.array_list.Managed(Declaration).init(alloc);
583
- try type_decls.ensureTotalCapacity(group_cap);
584
- var classes = std.array_list.Managed(Declaration).init(alloc);
585
- try classes.ensureTotalCapacity(group_cap);
586
- var enums = std.array_list.Managed(Declaration).init(alloc);
587
- try enums.ensureTotalCapacity(group_cap);
588
- var modules = std.array_list.Managed(Declaration).init(alloc);
589
- try modules.ensureTotalCapacity(group_cap);
590
- var exports = std.array_list.Managed(Declaration).init(alloc);
591
- try exports.ensureTotalCapacity(group_cap);
617
+ // Index-based grouping: store indices into the declarations array instead of
618
+ // copying Declaration structs. A single allocation for the index array replaces
619
+ // 9 separate ArrayList allocations, dramatically reducing overhead for small files.
620
+ const GROUP_COUNT = 9;
621
+ // Group order: imports(0), functions(1), variables(2), interfaces(3), types(4),
622
+ // classes(5), enums(6), modules(7), exports(8)
623
+ var group_counts = [_]u32{0} ** GROUP_COUNT;
592
624
 
625
+ // First pass: count declarations per group
593
626
  for (declarations) |d| {
594
- switch (d.kind) {
595
- .import_decl => try imports.append(d),
596
- .function_decl => try functions.append(d),
597
- .variable_decl => try variables.append(d),
598
- .interface_decl => try interfaces.append(d),
599
- .type_decl => try type_decls.append(d),
600
- .class_decl => try classes.append(d),
601
- .enum_decl => try enums.append(d),
602
- .module_decl, .namespace_decl => try modules.append(d),
603
- .export_decl => try exports.append(d),
604
- .unknown_decl => {},
605
- }
627
+ const g: usize = switch (d.kind) {
628
+ .import_decl => 0,
629
+ .function_decl => 1,
630
+ .variable_decl => 2,
631
+ .interface_decl => 3,
632
+ .type_decl => 4,
633
+ .class_decl => 5,
634
+ .enum_decl => 6,
635
+ .module_decl, .namespace_decl => 7,
636
+ .export_decl => 8,
637
+ .unknown_decl => continue,
638
+ };
639
+ group_counts[g] += 1;
640
+ }
641
+
642
+ // Compute offsets for each group in the flat index array. Reuse the
643
+ // cumulative running total as the final size — saves one O(GROUP_COUNT)
644
+ // pass over the counts.
645
+ var group_offsets = [_]u32{0} ** GROUP_COUNT;
646
+ var total_indexed: u32 = 0;
647
+ for (0..GROUP_COUNT) |g| {
648
+ group_offsets[g] = total_indexed;
649
+ total_indexed += group_counts[g];
606
650
  }
607
651
 
652
+ // Single allocation for all group indices
653
+ const group_indices = try alloc.alloc(u32, total_indexed);
654
+ var write_pos = [_]u32{0} ** GROUP_COUNT;
655
+ @memcpy(&write_pos, &group_offsets);
656
+
657
+ // Second pass: fill indices
658
+ for (declarations, 0..) |d, di| {
659
+ const g: usize = switch (d.kind) {
660
+ .import_decl => 0,
661
+ .function_decl => 1,
662
+ .variable_decl => 2,
663
+ .interface_decl => 3,
664
+ .type_decl => 4,
665
+ .class_decl => 5,
666
+ .enum_decl => 6,
667
+ .module_decl, .namespace_decl => 7,
668
+ .export_decl => 8,
669
+ .unknown_decl => continue,
670
+ };
671
+ group_indices[write_pos[g]] = @intCast(di);
672
+ write_pos[g] += 1;
673
+ }
674
+
675
+ // Helper to get a group's slice of declaration indices
676
+ const imports_idx = group_indices[group_offsets[0]..write_pos[0]];
677
+ const functions_idx = group_indices[group_offsets[1]..write_pos[1]];
678
+ const variables_idx = group_indices[group_offsets[2]..write_pos[2]];
679
+ const interfaces_idx = group_indices[group_offsets[3]..write_pos[3]];
680
+ const type_decls_idx = group_indices[group_offsets[4]..write_pos[4]];
681
+ const classes_idx = group_indices[group_offsets[5]..write_pos[5]];
682
+ const enums_idx = group_indices[group_offsets[6]..write_pos[6]];
683
+ const modules_idx = group_indices[group_offsets[7]..write_pos[7]];
684
+ const exports_idx = group_indices[group_offsets[8]..write_pos[8]];
685
+
608
686
  // Parse exports to track exported items
687
+ const exports_len = exports_idx.len;
609
688
  var exported_items = std.StringHashMap(void).init(alloc);
610
- try exported_items.ensureTotalCapacity(@intCast(@max(exports.items.len * 4, 8)));
689
+ try exported_items.ensureTotalCapacity(@intCast(@max(exports_len * 4, 8)));
611
690
  var type_export_stmts = std.array_list.Managed([]const u8).init(alloc);
612
- try type_export_stmts.ensureTotalCapacity(@max(exports.items.len / 2, 2));
691
+ try type_export_stmts.ensureTotalCapacity(@max(exports_len / 2, 2));
613
692
  var value_export_stmts = std.array_list.Managed([]const u8).init(alloc);
614
- try value_export_stmts.ensureTotalCapacity(@max(exports.items.len / 2, 2));
693
+ try value_export_stmts.ensureTotalCapacity(@max(exports_len / 2, 2));
615
694
  var default_exports = std.array_list.Managed([]const u8).init(alloc);
616
- var seen_exports = std.StringHashMap(void).init(alloc);
617
- try seen_exports.ensureTotalCapacity(@intCast(@max(exports.items.len, 4)));
695
+ // Use linear array for small export counts (faster than HashMap for <32 entries)
696
+ var seen_exports_arr = std.array_list.Managed([]const u8).init(alloc);
697
+ try seen_exports_arr.ensureTotalCapacity(@max(exports_len, 4));
618
698
 
619
699
  // Reusable buffer for building export statements (avoids per-iteration alloc)
620
700
  var stmt_buf = std.array_list.Managed(u8).init(alloc);
621
701
  try stmt_buf.ensureTotalCapacity(256);
622
702
 
623
- for (exports.items) |decl| {
703
+ for (exports_idx) |eidx| {
704
+ const decl = declarations[eidx];
624
705
  const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
625
706
 
626
707
  if (ch.startsWith(decl.text, "export default")) {
@@ -644,7 +725,8 @@ pub fn processDeclarations(
644
725
  if (ch.indexOfChar(export_text, '{', 0)) |brace_s| {
645
726
  if (ch.indexOfChar(export_text, '}', brace_s)) |brace_e| {
646
727
  const items_str = export_text[brace_s + 1 .. brace_e];
647
- var iter = std.mem.splitSequence(u8, items_str, ",");
728
+ // splitScalar is faster than splitSequence for a 1-byte separator.
729
+ var iter = std.mem.splitScalar(u8, items_str, ',');
648
730
  while (iter.next()) |item| {
649
731
  const trimmed_item = ch.sliceTrimmed(item, 0, item.len);
650
732
  if (trimmed_item.len > 0) {
@@ -654,14 +736,30 @@ pub fn processDeclarations(
654
736
  }
655
737
  }
656
738
 
739
+ // Determine type-only-ness BEFORE concatenation. Checking
740
+ // ch.contains on the full (comments + export_text) text scanned
741
+ // arbitrarily long comment text on every export.
742
+ const is_type_export = ch.startsWith(export_text, "export type");
743
+
657
744
  stmt_buf.clearRetainingCapacity();
658
745
  try stmt_buf.appendSlice(comments);
659
746
  try stmt_buf.appendSlice(export_text);
660
747
  const full_text = try stmt_buf.toOwnedSlice();
661
748
 
662
- const gop = try seen_exports.getOrPut(full_text);
663
- if (!gop.found_existing) {
664
- if (ch.contains(full_text, "export type")) {
749
+ // Linear dedup: faster than HashMap for typical export counts (<32).
750
+ // Length pre-check rejects most non-matches without invoking
751
+ // std.mem.eql, which compares byte-by-byte even on length mismatch.
752
+ var is_dup = false;
753
+ const ft_len = full_text.len;
754
+ for (seen_exports_arr.items) |seen| {
755
+ if (seen.len == ft_len and std.mem.eql(u8, seen, full_text)) {
756
+ is_dup = true;
757
+ break;
758
+ }
759
+ }
760
+ if (!is_dup) {
761
+ try seen_exports_arr.append(full_text);
762
+ if (is_type_export) {
665
763
  try type_export_stmts.append(full_text);
666
764
  } else {
667
765
  try value_export_stmts.append(full_text);
@@ -673,15 +771,18 @@ pub fn processDeclarations(
673
771
  // Short-circuit: skip combined_words building and import map when there are no imports.
674
772
  // For import-free code (e.g. synthetic benchmarks), this avoids O(n) word
675
773
  // extraction across all declarations — a significant saving on large inputs.
774
+ // Lazy-init: only allocate when there are interfaces to check
676
775
  var interface_references = std.StringHashMap(void).init(alloc);
776
+ _ = &interface_references; // prevent unused warning in no-imports path
677
777
  var processed_imports = std.array_list.Managed([]const u8).init(alloc);
678
778
 
679
- if (imports.items.len > 0) {
779
+ if (imports_idx.len > 0) {
680
780
  // Build import-item-to-declaration map (only when imports exist)
681
781
  const ImportDeclMap = std.StringHashMap(Declaration);
682
782
  var all_imported_items_map = ImportDeclMap.init(alloc);
683
- try all_imported_items_map.ensureTotalCapacity(@intCast(@max(imports.items.len * 4, 8)));
684
- for (imports.items) |imp| {
783
+ try all_imported_items_map.ensureTotalCapacity(@intCast(@max(imports_idx.len * 4, 8)));
784
+ for (imports_idx) |iidx| {
785
+ const imp = declarations[iidx];
685
786
  const items = extractAllImportedItems(imp);
686
787
  for (items) |item| {
687
788
  try all_imported_items_map.put(item, imp);
@@ -711,9 +812,10 @@ pub fn processDeclarations(
711
812
  }
712
813
 
713
814
  // Interface reference detection
714
- try interface_references.ensureTotalCapacity(@intCast(@max(interfaces.items.len, 4)));
715
- if (interfaces.items.len > 0 and combined_words.count() > 0) {
716
- for (interfaces.items) |iface| {
815
+ try interface_references.ensureTotalCapacity(@intCast(@max(interfaces_idx.len, 4)));
816
+ if (interfaces_idx.len > 0 and combined_words.count() > 0) {
817
+ for (interfaces_idx) |iidx| {
818
+ const iface = declarations[iidx];
717
819
  if (combined_words.contains(iface.name)) {
718
820
  try interface_references.put(iface.name, {});
719
821
  }
@@ -721,13 +823,14 @@ pub fn processDeclarations(
721
823
  }
722
824
 
723
825
  // Add interface text to combined_words (after ref detection, before import filtering)
724
- for (interfaces.items) |iface| {
826
+ for (interfaces_idx) |iidx| {
827
+ const iface = declarations[iidx];
725
828
  if (iface.is_exported or interface_references.contains(iface.name)) {
726
829
  extractWords(&combined_words, iface.text);
727
830
  }
728
831
  }
729
832
  var used_import_items = std.StringHashMap(void).init(alloc);
730
- try used_import_items.ensureTotalCapacity(@intCast(@max(imports.items.len * 4, 8)));
833
+ try used_import_items.ensureTotalCapacity(@intCast(@max(imports_idx.len * 4, 8)));
731
834
 
732
835
  if (combined_words.count() > 0) {
733
836
  var map_iter = all_imported_items_map.keyIterator();
@@ -747,14 +850,15 @@ pub fn processDeclarations(
747
850
  }
748
851
 
749
852
  // Filter and rebuild imports
750
- try processed_imports.ensureTotalCapacity(imports.items.len);
853
+ try processed_imports.ensureTotalCapacity(imports_idx.len);
751
854
  // Reusable buffer for building import statements
752
855
  var import_buf = std.array_list.Managed(u8).init(alloc);
753
856
  try import_buf.ensureTotalCapacity(256);
754
857
  var used_named = std.array_list.Managed([]const u8).init(alloc);
755
858
  try used_named.ensureTotalCapacity(16);
756
859
 
757
- for (imports.items) |imp| {
860
+ for (imports_idx) |iidx| {
861
+ const imp = declarations[iidx];
758
862
  // Preserve side-effect imports
759
863
  if (imp.is_side_effect) {
760
864
  const trimmed_imp = ch.sliceTrimmed(imp.text, 0, imp.text.len);
@@ -795,16 +899,20 @@ pub fn processDeclarations(
795
899
  if (parsed.default_name) |dn| try import_buf.appendSlice(dn);
796
900
  if (used_named.items.len > 0) {
797
901
  try import_buf.appendSlice(", { ");
798
- for (used_named.items, 0..) |ni, idx| {
799
- if (idx > 0) try import_buf.appendSlice(", ");
902
+ // Emit first item, then comma-prefixed subsequent items —
903
+ // skips the per-iteration `if (idx > 0)` branch.
904
+ try import_buf.appendSlice(used_named.items[0]);
905
+ for (used_named.items[1..]) |ni| {
906
+ try import_buf.appendSlice(", ");
800
907
  try import_buf.appendSlice(ni);
801
908
  }
802
909
  try import_buf.appendSlice(" }");
803
910
  }
804
911
  } else if (used_named.items.len > 0) {
805
912
  try import_buf.appendSlice("{ ");
806
- for (used_named.items, 0..) |ni, idx| {
807
- if (idx > 0) try import_buf.appendSlice(", ");
913
+ try import_buf.appendSlice(used_named.items[0]);
914
+ for (used_named.items[1..]) |ni| {
915
+ try import_buf.appendSlice(", ");
808
916
  try import_buf.appendSlice(ni);
809
917
  }
810
918
  try import_buf.appendSlice(" }");
@@ -818,61 +926,70 @@ pub fn processDeclarations(
818
926
  }
819
927
  }
820
928
 
821
- // Sort imports by priority then locale-aware alphabetical
929
+ // Sort imports by priority then locale-aware alphabetical.
930
+ // Pre-compute priorities once (avoids re-scanning during O(n log n) sort).
822
931
  if (processed_imports.items.len > 1) {
823
- const SortCtx = struct {
824
- import_order_items: []const []const u8,
825
- default_priority: usize,
826
-
827
- pub fn priority(self: @This(), imp: []const u8) usize {
828
- for (self.import_order_items, 0..) |p, idx| {
829
- var found = false;
830
- if (ch.indexOf(imp, "from '", 0)) |fi| {
831
- if (ch.indexOf(imp, p, fi + 6) != null) found = true;
832
- }
833
- if (!found) {
834
- if (ch.indexOf(imp, "from \"", 0)) |fi| {
835
- if (ch.indexOf(imp, p, fi + 6) != null) found = true;
836
- }
837
- }
838
- if (found) return idx;
932
+ const default_priority = import_order.len;
933
+ const priorities = try alloc.alloc(usize, processed_imports.items.len);
934
+ for (processed_imports.items, 0..) |imp, pi| {
935
+ // Locate the source-string slice. Single scan via indexOf for
936
+ // the " from " marker, then dispatch on the quote byte we find
937
+ // there avoids two separate searches for the 7-byte needle.
938
+ var src_start: usize = 0;
939
+ var src_end: usize = imp.len;
940
+ if (ch.indexOf(imp, " from ", 0)) |fi| {
941
+ var qi = fi + 6;
942
+ while (qi < imp.len and (imp[qi] == ' ' or imp[qi] == '\t')) qi += 1;
943
+ if (qi < imp.len and (imp[qi] == '\'' or imp[qi] == '"')) {
944
+ const quote = imp[qi];
945
+ src_start = qi + 1;
946
+ if (ch.indexOfChar(imp, quote, src_start)) |qe| src_end = qe;
839
947
  }
840
- return self.default_priority;
841
948
  }
949
+ const src = if (src_start < src_end) imp[src_start..src_end] else imp;
950
+ var prio = default_priority;
951
+ for (import_order, 0..) |p, idx| {
952
+ if (ch.indexOf(src, p, 0) != null) { prio = idx; break; }
953
+ }
954
+ priorities[pi] = prio;
955
+ }
956
+
957
+ // Sort import indices by (priority, locale-order)
958
+ const indices = try alloc.alloc(usize, processed_imports.items.len);
959
+ for (0..indices.len) |ii| indices[ii] = ii;
960
+
961
+ const SortCtx2 = struct {
962
+ prios: []const usize,
963
+ items: []const []const u8,
842
964
 
843
- /// Locale-aware char sort key: symbols < digits < letters
844
- /// Matches JavaScript's localeCompare behavior
845
965
  fn charSortKey(c_val: u8) u32 {
846
966
  if (c_val >= 'a' and c_val <= 'z') return @as(u32, c_val - 'a') * 4 + 1000;
847
967
  if (c_val >= 'A' and c_val <= 'Z') return @as(u32, c_val - 'A') * 4 + 1001;
848
968
  if (c_val >= '0' and c_val <= '9') return @as(u32, c_val - '0') + 500;
849
969
  return @as(u32, c_val);
850
970
  }
971
+ };
851
972
 
852
- pub fn localeCompare(_: @This(), a: []const u8, b: []const u8) bool {
973
+ const sort_ctx = SortCtx2{ .prios = priorities, .items = processed_imports.items };
974
+ std.mem.sort(usize, indices, sort_ctx, struct {
975
+ fn lessThan(ctx: SortCtx2, ai: usize, bi: usize) bool {
976
+ if (ctx.prios[ai] != ctx.prios[bi]) return ctx.prios[ai] < ctx.prios[bi];
977
+ const a = ctx.items[ai];
978
+ const b = ctx.items[bi];
853
979
  const min_len = @min(a.len, b.len);
854
980
  for (0..min_len) |i| {
855
- const ak = charSortKey(a[i]);
856
- const bk = charSortKey(b[i]);
981
+ const ak = SortCtx2.charSortKey(a[i]);
982
+ const bk = SortCtx2.charSortKey(b[i]);
857
983
  if (ak != bk) return ak < bk;
858
984
  }
859
985
  return a.len < b.len;
860
986
  }
861
- };
862
-
863
- const ctx = SortCtx{
864
- .import_order_items = import_order,
865
- .default_priority = import_order.len,
866
- };
867
-
868
- std.mem.sort([]const u8, processed_imports.items, ctx, struct {
869
- fn lessThan(c_ctx: SortCtx, a: []const u8, b: []const u8) bool {
870
- const ap = c_ctx.priority(a);
871
- const bp = c_ctx.priority(b);
872
- if (ap != bp) return ap < bp;
873
- return c_ctx.localeCompare(a, b);
874
- }
875
987
  }.lessThan);
988
+
989
+ // Reorder in-place using sorted indices
990
+ const sorted = try alloc.alloc([]const u8, processed_imports.items.len);
991
+ for (indices, 0..) |idx_val, di| sorted[di] = processed_imports.items[idx_val];
992
+ @memcpy(processed_imports.items, sorted);
876
993
  }
877
994
  }
878
995
 
@@ -889,18 +1006,19 @@ pub fn processDeclarations(
889
1006
  }
890
1007
 
891
1008
  // Emit declaration groups: functions, variables, interfaces, types, classes, enums, modules
892
- const decl_groups = [_]struct { items: []const Declaration, kind_tag: u8 }{
893
- .{ .items = functions.items, .kind_tag = 'f' },
894
- .{ .items = variables.items, .kind_tag = 'v' },
895
- .{ .items = interfaces.items, .kind_tag = 'i' },
896
- .{ .items = type_decls.items, .kind_tag = 't' },
897
- .{ .items = classes.items, .kind_tag = 'c' },
898
- .{ .items = enums.items, .kind_tag = 'e' },
899
- .{ .items = modules.items, .kind_tag = 'm' },
1009
+ const decl_group_indices = [_][]const u32{
1010
+ functions_idx,
1011
+ variables_idx,
1012
+ interfaces_idx,
1013
+ type_decls_idx,
1014
+ classes_idx,
1015
+ enums_idx,
1016
+ modules_idx,
900
1017
  };
901
1018
 
902
- for (decl_groups) |group| {
903
- for (group.items) |decl| {
1019
+ for (decl_group_indices) |group_idx| {
1020
+ for (group_idx) |didx| {
1021
+ const decl = declarations[didx];
904
1022
  switch (decl.kind) {
905
1023
  .function_decl, .class_decl => {
906
1024
  // Direct emit: write comments + text straight to result buffer
@@ -1043,3 +1161,30 @@ test "extractTripleSlashDirectives" {
1043
1161
  try std.testing.expectEqualStrings("/// <reference types=\"node\" />", directives[0]);
1044
1162
  }
1045
1163
  }
1164
+
1165
+ test "extractTripleSlashDirectives recognizes amd-module and amd-dependency" {
1166
+ const alloc = std.testing.allocator;
1167
+ const src = "/// <amd-module name=\"m\" />\n/// <amd-dependency path=\"./d\" />\nexport {};";
1168
+ const directives = try extractTripleSlashDirectives(alloc, src);
1169
+ defer alloc.free(directives);
1170
+ try std.testing.expectEqual(@as(usize, 2), directives.len);
1171
+ try std.testing.expect(std.mem.indexOf(u8, directives[0], "amd-module") != null);
1172
+ try std.testing.expect(std.mem.indexOf(u8, directives[1], "amd-dependency") != null);
1173
+ }
1174
+
1175
+ test "extractTripleSlashDirectives skips triple-slash comments without `<` (fast pre-filter)" {
1176
+ const alloc = std.testing.allocator;
1177
+ const src = "/// just a comment, no angle bracket\nexport {};";
1178
+ const directives = try extractTripleSlashDirectives(alloc, src);
1179
+ defer alloc.free(directives);
1180
+ try std.testing.expectEqual(@as(usize, 0), directives.len);
1181
+ }
1182
+
1183
+ test "extractTripleSlashDirectives stops at first non-comment statement" {
1184
+ const alloc = std.testing.allocator;
1185
+ const src = "/// <reference types=\"node\" />\nexport const x = 1;\n/// <reference types=\"bun\" />";
1186
+ const directives = try extractTripleSlashDirectives(alloc, src);
1187
+ defer alloc.free(directives);
1188
+ try std.testing.expectEqual(@as(usize, 1), directives.len);
1189
+ try std.testing.expect(std.mem.indexOf(u8, directives[0], "node") != null);
1190
+ }