@stacksjs/zig-dtsx 0.9.13 → 0.9.16

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.
@@ -83,6 +83,9 @@ fn countOccurrences(haystack: []const u8, needle: []const u8) usize {
83
83
  /// Parse array elements handling nested structures.
84
84
  /// Returns slices into the original content string.
85
85
  pub fn parseArrayElements(alloc: std.mem.Allocator, content: []const u8) InferError![][]const u8 {
86
+ // Fast path: empty content
87
+ if (content.len == 0) return &.{};
88
+
86
89
  var elements = std.array_list.Managed([]const u8).init(alloc);
87
90
  // Pre-size: estimate element count from top-level commas
88
91
  var est: usize = 1;
@@ -132,16 +135,17 @@ pub fn parseArrayElements(alloc: std.mem.Allocator, content: []const u8) InferEr
132
135
  try elements.append(last);
133
136
  }
134
137
 
135
- return elements.items;
138
+ // toOwnedSlice() trims unused capacity — important for non-arena callers.
139
+ return elements.toOwnedSlice();
136
140
  }
137
141
 
138
142
  /// Clean a method signature: strip async, replace defaults with ?, collapse whitespace.
139
143
  /// Single-pass implementation combining all transformations.
140
144
  fn cleanMethodSignature(alloc: std.mem.Allocator, signature: []const u8) InferError![]const u8 {
141
145
  var input = signature;
142
- // Remove leading "async "
146
+ // Remove leading "async " (6 chars including the trailing space)
143
147
  if (ch.startsWith(input, "async ")) {
144
- input = trim(input[5..]);
148
+ input = trim(input[6..]);
145
149
  }
146
150
 
147
151
  // Fast path: if no async, no defaults (=), no consecutive whitespace, return as-is
@@ -238,7 +242,11 @@ fn cleanMethodSignature(alloc: std.mem.Allocator, signature: []const u8) InferEr
238
242
  /// Strip 'async' keywords from a string without collapsing whitespace.
239
243
  /// Used when we want to remove async modifiers but preserve multiline formatting.
240
244
  fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]const u8 {
245
+ // Fast-path: no "async" substring at all → return input unchanged (zero-copy).
246
+ if (std.mem.indexOf(u8, input, "async") == null) return input;
247
+
241
248
  var buf = std.array_list.Managed(u8).init(alloc);
249
+ try buf.ensureTotalCapacity(input.len);
242
250
  var j: usize = 0;
243
251
  // Remove leading "async "
244
252
  if (ch.startsWith(input, "async ")) {
@@ -260,7 +268,9 @@ fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]c
260
268
  try buf.append(input[j]);
261
269
  j += 1;
262
270
  }
263
- return buf.items;
271
+ // toOwnedSlice trims the unused capacity — important when the caller
272
+ // wraps this in a non-arena allocator.
273
+ return buf.toOwnedSlice();
264
274
  }
265
275
 
266
276
  /// Convert a method definition to a function type.
@@ -268,9 +278,9 @@ fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]c
268
278
  /// Output: "generics(params) => ReturnType"
269
279
  fn convertMethodToFunctionType(alloc: std.mem.Allocator, key: []const u8, method_def: []const u8) InferError![]const u8 {
270
280
  var cleaned = method_def;
271
- // Remove leading async
281
+ // Remove leading async (6 chars including the trailing space)
272
282
  if (ch.startsWith(cleaned, "async ")) {
273
- cleaned = trim(cleaned[5..]);
283
+ cleaned = trim(cleaned[6..]);
274
284
  }
275
285
 
276
286
  // Extract generics from key (e.g., "onSuccess<T>" -> generics = "<T>")
@@ -323,7 +333,10 @@ fn convertMethodToFunctionType(alloc: std.mem.Allocator, key: []const u8, method
323
333
 
324
334
  /// Clean parameter defaults: replace `param = value` with `param?`
325
335
  fn cleanParameterDefaults(alloc: std.mem.Allocator, params: []const u8) InferError![]const u8 {
336
+ // Fast path: if there's no '=' anywhere, the input is already clean.
337
+ if (std.mem.indexOfScalar(u8, params, '=') == null) return params;
326
338
  var buf = std.array_list.Managed(u8).init(alloc);
339
+ try buf.ensureTotalCapacity(params.len);
327
340
  var j: usize = 0;
328
341
  while (j < params.len) {
329
342
  // Try to match word= pattern (not =>)
@@ -356,7 +369,8 @@ fn cleanParameterDefaults(alloc: std.mem.Allocator, params: []const u8) InferErr
356
369
  j += 1;
357
370
  }
358
371
  }
359
- return buf.items;
372
+ // toOwnedSlice() trims unused capacity — safer for non-arena allocators.
373
+ return buf.toOwnedSlice();
360
374
  }
361
375
 
362
376
  /// Parse object properties from content between braces.
@@ -385,10 +399,11 @@ fn parseObjectProperties(alloc: std.mem.Allocator, content: []const u8) InferErr
385
399
  const prev = if (i > 0) content[i - 1] else @as(u8, 0);
386
400
  const next = if (i + 1 < content.len) content[i + 1] else @as(u8, 0);
387
401
 
388
- // Track single-line comments — skip to end of line
402
+ // Track single-line comments — SIMD scan to end of line via indexOfChar
403
+ // instead of a byte-by-byte loop.
389
404
  if (!in_string and !in_comment and c == '/' and next == '/') {
390
- i += 2; // Skip '//'
391
- while (i < content.len and content[i] != '\n') : (i += 1) {}
405
+ const nl = ch.indexOfChar(content, '\n', i + 2);
406
+ i = if (nl) |n| n else content.len;
392
407
  // Update current_start if in key mode so the key slice doesn't include comment text
393
408
  if (in_key and i < content.len) {
394
409
  current_start = i + 1;
@@ -479,7 +494,8 @@ fn parseObjectProperties(alloc: std.mem.Allocator, content: []const u8) InferErr
479
494
  }
480
495
  }
481
496
 
482
- return properties.items;
497
+ // toOwnedSlice() trims unused capacity — important for non-arena callers.
498
+ return properties.toOwnedSlice();
483
499
  }
484
500
 
485
501
  /// Find matching bracket (open/close) starting from `start`, skipping strings and comments.
@@ -488,33 +504,30 @@ fn findMatchingBracket(str: []const u8, start: usize, open: u8, close: u8) ?usiz
488
504
  var i = start;
489
505
  while (i < str.len) : (i += 1) {
490
506
  const c = str[i];
491
- // Skip string literals
507
+ // Skip string literals — handle backslash escapes, otherwise SIMD-scan
508
+ // for the next quote/backslash byte.
492
509
  if (c == '"' or c == '\'' or c == '`') {
493
510
  i += 1;
494
- while (i < str.len) : (i += 1) {
495
- if (str[i] == '\\') {
496
- i += 1; // skip escaped char
497
- continue;
498
- }
511
+ while (i < str.len) {
512
+ if (str[i] == '\\') { i += 2; continue; }
499
513
  if (str[i] == c) break;
514
+ i += 1;
500
515
  }
501
516
  continue;
502
517
  }
503
- // Skip line comments
518
+ // Skip line comments — SIMD-scan to the next newline instead of walking
519
+ // bytes one at a time.
504
520
  if (c == '/' and i + 1 < str.len and str[i + 1] == '/') {
505
- i += 2;
506
- while (i < str.len and str[i] != '\n') : (i += 1) {}
521
+ const nl = ch.indexOfChar(str, '\n', i + 2);
522
+ i = if (nl) |n| n else str.len;
523
+ if (i == str.len) break;
507
524
  continue;
508
525
  }
509
- // Skip block comments
526
+ // Skip block comments — indexOf jumps directly to `*/` via SIMD first-byte scan.
510
527
  if (c == '/' and i + 1 < str.len and str[i + 1] == '*') {
511
- i += 2;
512
- while (i + 1 < str.len) : (i += 1) {
513
- if (str[i] == '*' and str[i + 1] == '/') {
514
- i += 1;
515
- break;
516
- }
517
- }
528
+ const end = ch.indexOf(str, "*/", i + 2);
529
+ i = if (end) |e| e + 1 else str.len;
530
+ if (i == str.len) break;
518
531
  continue;
519
532
  }
520
533
  if (c == open) {
@@ -539,7 +552,6 @@ fn findMainArrowIndex(str: []const u8) ?usize {
539
552
  var i: usize = 0;
540
553
  while (i + 1 < str.len) : (i += 1) {
541
554
  const c = str[i];
542
- const prev = if (i > 0) str[i - 1] else @as(u8, 0);
543
555
 
544
556
  if (in_string) {
545
557
  if (c == '\\') {
@@ -553,7 +565,6 @@ fn findMainArrowIndex(str: []const u8) ?usize {
553
565
  if (c == '"' or c == '\'' or c == '`') {
554
566
  in_string = true;
555
567
  string_char = c;
556
- _ = prev;
557
568
  continue;
558
569
  }
559
570
 
@@ -622,14 +633,34 @@ pub fn inferNarrowType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
622
633
  const trimmed = trim(value);
623
634
  if (trimmed.len == 0) return "unknown";
624
635
 
636
+ // Fast path: if first char is a digit, it's almost certainly a number or BigInt literal
637
+ if (trimmed[0] >= '0' and trimmed[0] <= '9') {
638
+ if (isNumericLiteral(trimmed)) {
639
+ return if (!is_const) "number" else trimmed;
640
+ }
641
+ // BigInt literal: digits followed by 'n'
642
+ if (trimmed.len > 1 and trimmed[trimmed.len - 1] == 'n' and isBigIntDigits(trimmed)) {
643
+ return if (is_const) trimmed else "bigint";
644
+ }
645
+ return "unknown";
646
+ }
647
+
648
+ // Fast path: negative numbers
649
+ if (trimmed[0] == '-' and trimmed.len > 1 and trimmed[1] >= '0' and trimmed[1] <= '9') {
650
+ if (isNumericLiteral(trimmed)) {
651
+ return if (!is_const) "number" else trimmed;
652
+ }
653
+ return "unknown";
654
+ }
655
+
625
656
  // BigInt expressions
626
657
  if (ch.startsWith(trimmed, "BigInt(")) return "bigint";
627
658
 
628
659
  // Symbol.for
629
660
  if (ch.startsWith(trimmed, "Symbol.for(")) return "symbol";
630
661
 
631
- // Single-pass scan for substring hints (replaces 5 separate ch.contains calls)
632
- const hints = ValueHints.scan(trimmed);
662
+ // Single-pass scan for substring hints skip for short values where hints can't appear
663
+ const hints = if (trimmed.len >= 4) ValueHints.scan(trimmed) else ValueHints{};
633
664
 
634
665
  // Tagged template literals
635
666
  if (hints.has_raw_template) return "string";
@@ -653,15 +684,17 @@ pub fn inferNarrowType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
653
684
  return trimmed;
654
685
  }
655
686
 
656
- // Boolean literals
657
- if (std.mem.eql(u8, trimmed, "true") or std.mem.eql(u8, trimmed, "false")) {
658
- if (!is_const) return "boolean";
659
- return trimmed;
687
+ // Boolean literals (length-first to skip most comparisons)
688
+ if (trimmed.len == 4 and std.mem.eql(u8, trimmed, "true")) {
689
+ return if (!is_const) "boolean" else trimmed;
690
+ }
691
+ if (trimmed.len == 5 and std.mem.eql(u8, trimmed, "false")) {
692
+ return if (!is_const) "boolean" else trimmed;
660
693
  }
661
694
 
662
- // Null and undefined
663
- if (std.mem.eql(u8, trimmed, "null")) return "null";
664
- if (std.mem.eql(u8, trimmed, "undefined")) return "undefined";
695
+ // Null and undefined (length-first)
696
+ if (trimmed.len == 4 and std.mem.eql(u8, trimmed, "null")) return "null";
697
+ if (trimmed.len == 9 and std.mem.eql(u8, trimmed, "undefined")) return "undefined";
665
698
 
666
699
  // Array literals
667
700
  if (trimmed[0] == '[' and trimmed[trimmed.len - 1] == ']') {
@@ -871,14 +904,9 @@ pub fn inferArrayType(alloc: std.mem.Allocator, value: []const u8, is_const: boo
871
904
  try unique_set.put(t, {});
872
905
  try unique.append(t);
873
906
  }
874
- // Literal check
875
- if (all_literals) {
876
- const is_literal = isNumericLiteral(t) or
877
- std.mem.eql(u8, t, "true") or std.mem.eql(u8, t, "false") or
878
- (t.len >= 2 and t[0] == '"' and t[t.len - 1] == '"') or
879
- (t.len >= 2 and t[0] == '\'' and t[t.len - 1] == '\'');
880
- if (!is_literal) all_literals = false;
881
- }
907
+ // Literal check — reuse isPrimitiveLiteral, which first-byte-dispatches
908
+ // through string/boolean/numeric branches without retesting other kinds.
909
+ if (all_literals and !isPrimitiveLiteral(t)) all_literals = false;
882
910
  }
883
911
 
884
912
  if (all_literals and types.len <= 10) {
@@ -911,16 +939,28 @@ pub fn inferArrayType(alloc: std.mem.Allocator, value: []const u8, is_const: boo
911
939
 
912
940
  /// Check if a value string is a primitive literal (number, string, boolean)
913
941
  fn isPrimitiveLiteral(val: []const u8) bool {
914
- if (isNumericLiteral(val)) return true;
915
- if (std.mem.eql(u8, val, "true") or std.mem.eql(u8, val, "false")) return true;
916
- if (val.len >= 2 and ((val[0] == '"' and val[val.len - 1] == '"') or
917
- (val[0] == '\'' and val[val.len - 1] == '\''))) return true;
918
- return false;
942
+ if (val.len == 0) return false;
943
+ // First-byte dispatch: each kind of literal has a distinct prefix byte —
944
+ // saves running every check on every value.
945
+ return switch (val[0]) {
946
+ '"' => val.len >= 2 and val[val.len - 1] == '"',
947
+ '\'' => val.len >= 2 and val[val.len - 1] == '\'',
948
+ 't' => val.len == 4 and std.mem.eql(u8, val, "true"),
949
+ 'f' => val.len == 5 and std.mem.eql(u8, val, "false"),
950
+ '-', '0'...'9' => isNumericLiteral(val),
951
+ else => false,
952
+ };
919
953
  }
920
954
 
921
955
  /// Check if a type is a base/widened type
922
956
  fn isBaseType(t: []const u8) bool {
923
- return std.mem.eql(u8, t, "number") or std.mem.eql(u8, t, "string") or std.mem.eql(u8, t, "boolean");
957
+ // Length-first dispatch: number=6, string=6, boolean=7. Reject all other
958
+ // lengths in O(1) without invoking std.mem.eql.
959
+ return switch (t.len) {
960
+ 6 => std.mem.eql(u8, t, "number") or std.mem.eql(u8, t, "string"),
961
+ 7 => std.mem.eql(u8, t, "boolean"),
962
+ else => false,
963
+ };
924
964
  }
925
965
 
926
966
  /// Check if an array literal only contains primitives/nested arrays/objects (no runtime expressions)
@@ -960,20 +1000,19 @@ fn isSimpleArrayDefault(val: []const u8) bool {
960
1000
  continue;
961
1001
  }
962
1002
  if (j < val.len and val[j] == '(') return false; // function call
963
- if (std.mem.eql(u8, word, "new") or
964
- std.mem.eql(u8, word, "console") or
965
- std.mem.eql(u8, word, "process") or
966
- std.mem.eql(u8, word, "async") or
967
- std.mem.eql(u8, word, "await") or
968
- std.mem.eql(u8, word, "function") or
969
- std.mem.eql(u8, word, "yield")) return false;
970
- // Bare identifiers that aren't true/false are also runtime refs
971
- if (!std.mem.eql(u8, word, "true") and
972
- !std.mem.eql(u8, word, "false") and
973
- !std.mem.eql(u8, word, "null") and
974
- !std.mem.eql(u8, word, "undefined") and
975
- !std.mem.eql(u8, word, "const") and
976
- !std.mem.eql(u8, word, "as")) return false;
1003
+ // Identifier classification dispatched on first byte — replaces 13
1004
+ // sequential std.mem.eql calls. Anything not on the allow-list (true,
1005
+ // false, null, undefined, const, as) is treated as a runtime reference.
1006
+ const word_ok = wo: switch (word[0]) {
1007
+ 't' => break :wo std.mem.eql(u8, word, "true"),
1008
+ 'f' => break :wo std.mem.eql(u8, word, "false"),
1009
+ 'n' => break :wo std.mem.eql(u8, word, "null"),
1010
+ 'u' => break :wo std.mem.eql(u8, word, "undefined"),
1011
+ 'c' => break :wo std.mem.eql(u8, word, "const"),
1012
+ 'a' => break :wo std.mem.eql(u8, word, "as"),
1013
+ else => break :wo false,
1014
+ };
1015
+ if (!word_ok) return false;
977
1016
  if (i > 0) i -= 1; // back up since outer loop will increment
978
1017
  }
979
1018
  }
@@ -1084,11 +1123,18 @@ pub fn inferObjectType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
1084
1123
  const nested_default = _clean_default_result;
1085
1124
  _clean_default_result = saved_default; // restore parent's
1086
1125
 
1087
- // Clean method signatures in inferred types
1088
- if (ch.contains(val_type, "=>")) {
1089
- val_type = try cleanMethodSignature(alloc, val_type);
1090
- } else if (ch.contains(val_type, "async")) {
1091
- val_type = try stripAsyncKeyword(alloc, val_type);
1126
+ // Clean method signatures in inferred types — single scan for the
1127
+ // first interesting byte. Most val_types are bare types like "string"
1128
+ // or "number" that contain neither marker.
1129
+ if (ch.indexOfChar(val_type, '=', 0)) |_| {
1130
+ // Confirm '=>' rather than just '='
1131
+ if (ch.indexOf(val_type, "=>", 0) != null) {
1132
+ val_type = try cleanMethodSignature(alloc, val_type);
1133
+ }
1134
+ } else if (ch.indexOfChar(val_type, 'a', 0)) |_| {
1135
+ if (ch.indexOf(val_type, "async", 0) != null) {
1136
+ val_type = try stripAsyncKeyword(alloc, val_type);
1137
+ }
1092
1138
  }
1093
1139
 
1094
1140
  // Add inline @defaultValue for widened primitive properties
@@ -1159,23 +1205,29 @@ pub fn inferObjectType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
1159
1205
  const indent = if (depth > 0) (depth - 1) / 2 else 0;
1160
1206
  const pad_size = (indent + 1) * 2;
1161
1207
  const close_pad_size = indent * 2;
1162
- var ml = std.array_list.Managed(u8).init(alloc);
1163
- try ml.appendSlice("{\n");
1208
+ // Pre-compute total length so we can do one alloc + memcpy/memset.
1209
+ var total: usize = 2; // "{\n"
1210
+ for (clean_props.items) |item| total += pad_size + item.len + 1; // pad + item + '\n'
1211
+ // Each non-final line gets a comma before its newline.
1212
+ if (clean_props.items.len > 0) total += clean_props.items.len - 1;
1213
+ total += close_pad_size + 1; // close pad + '}'
1214
+
1215
+ const ml_buf = try alloc.alloc(u8, total);
1216
+ var mp: usize = 0;
1217
+ ml_buf[mp] = '{'; mp += 1;
1218
+ ml_buf[mp] = '\n'; mp += 1;
1164
1219
  for (clean_props.items, 0..) |item, ci| {
1165
- {
1166
- var p: usize = 0;
1167
- while (p < pad_size) : (p += 1) try ml.append(' ');
1168
- }
1169
- try ml.appendSlice(item);
1170
- if (ci < clean_props.items.len - 1) try ml.append(',');
1171
- try ml.append('\n');
1172
- }
1173
- {
1174
- var p: usize = 0;
1175
- while (p < close_pad_size) : (p += 1) try ml.append(' ');
1220
+ @memset(ml_buf[mp..][0..pad_size], ' ');
1221
+ mp += pad_size;
1222
+ @memcpy(ml_buf[mp..][0..item.len], item);
1223
+ mp += item.len;
1224
+ if (ci < clean_props.items.len - 1) { ml_buf[mp] = ','; mp += 1; }
1225
+ ml_buf[mp] = '\n'; mp += 1;
1176
1226
  }
1177
- try ml.append('}');
1178
- _clean_default_result = try ml.toOwnedSlice();
1227
+ @memset(ml_buf[mp..][0..close_pad_size], ' ');
1228
+ mp += close_pad_size;
1229
+ ml_buf[mp] = '}'; mp += 1;
1230
+ _clean_default_result = ml_buf[0..mp];
1179
1231
  }
1180
1232
  }
1181
1233
 
@@ -1204,28 +1256,35 @@ fn inferNewExpressionType(alloc: std.mem.Allocator, value: []const u8) InferErro
1204
1256
  }
1205
1257
  }
1206
1258
 
1207
- // Fallback for known built-in types
1208
- if (std.mem.eql(u8, class_name, "Date")) return "Date";
1209
- if (std.mem.eql(u8, class_name, "Map")) return "Map<any, any>";
1210
- if (std.mem.eql(u8, class_name, "Set")) return "Set<any>";
1211
- if (std.mem.eql(u8, class_name, "WeakMap")) return "WeakMap<any, any>";
1212
- if (std.mem.eql(u8, class_name, "WeakSet")) return "WeakSet<any>";
1213
- if (std.mem.eql(u8, class_name, "RegExp")) return "RegExp";
1214
- if (std.mem.eql(u8, class_name, "Error")) return "Error";
1215
- if (std.mem.eql(u8, class_name, "Array")) return "any[]";
1216
- if (std.mem.eql(u8, class_name, "Object")) return "object";
1217
- if (std.mem.eql(u8, class_name, "Function")) return "Function";
1218
- if (std.mem.eql(u8, class_name, "Promise")) return "Promise<any>";
1219
-
1220
- return class_name;
1259
+ // Fallback for known built-in types — first-byte dispatch avoids 11 sequential
1260
+ // std.mem.eql calls. For unknown class names we fall through to returning
1261
+ // the original identifier directly.
1262
+ if (class_name.len == 0) return class_name;
1263
+ return switch (class_name[0]) {
1264
+ 'A' => if (std.mem.eql(u8, class_name, "Array")) "any[]" else class_name,
1265
+ 'D' => if (std.mem.eql(u8, class_name, "Date")) "Date" else class_name,
1266
+ 'E' => if (std.mem.eql(u8, class_name, "Error")) "Error" else class_name,
1267
+ 'F' => if (std.mem.eql(u8, class_name, "Function")) "Function" else class_name,
1268
+ 'M' => if (std.mem.eql(u8, class_name, "Map")) "Map<any, any>" else class_name,
1269
+ 'O' => if (std.mem.eql(u8, class_name, "Object")) "object" else class_name,
1270
+ 'P' => if (std.mem.eql(u8, class_name, "Promise")) "Promise<any>" else class_name,
1271
+ 'R' => if (std.mem.eql(u8, class_name, "RegExp")) "RegExp" else class_name,
1272
+ 'S' => if (std.mem.eql(u8, class_name, "Set")) "Set<any>" else class_name,
1273
+ 'W' => blk: {
1274
+ if (std.mem.eql(u8, class_name, "WeakMap")) break :blk "WeakMap<any, any>";
1275
+ if (std.mem.eql(u8, class_name, "WeakSet")) break :blk "WeakSet<any>";
1276
+ break :blk class_name;
1277
+ },
1278
+ else => class_name,
1279
+ };
1221
1280
  }
1222
1281
 
1223
1282
  /// Infer type from Promise expression
1224
1283
  fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
1225
1284
  if (ch.startsWith(value, "Promise.resolve(")) {
1226
- // Extract argument
1227
- const paren_start = std.mem.indexOf(u8, value, "(") orelse return "Promise<unknown>";
1228
- const paren_end = std.mem.lastIndexOf(u8, value, ")") orelse return "Promise<unknown>";
1285
+ // Extract argument — single-byte searches use the SIMD scalar paths.
1286
+ const paren_start = std.mem.indexOfScalar(u8, value, '(') orelse return "Promise<unknown>";
1287
+ const paren_end = std.mem.lastIndexOfScalar(u8, value, ')') orelse return "Promise<unknown>";
1229
1288
  if (paren_end > paren_start + 1) {
1230
1289
  const arg = trim(value[paren_start + 1 .. paren_end]);
1231
1290
  // Promise resolved values are immutable, so preserve is_const from context
@@ -1241,8 +1300,8 @@ fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool,
1241
1300
  if (ch.startsWith(value, "Promise.reject(")) return "Promise<never>";
1242
1301
  if (ch.startsWith(value, "Promise.all(")) {
1243
1302
  // Extract the array argument and infer element types
1244
- const paren_start = std.mem.indexOf(u8, value, "(") orelse return "Promise<unknown[]>";
1245
- const paren_end = std.mem.lastIndexOf(u8, value, ")") orelse return "Promise<unknown[]>";
1303
+ const paren_start = std.mem.indexOfScalar(u8, value, '(') orelse return "Promise<unknown[]>";
1304
+ const paren_end = std.mem.lastIndexOfScalar(u8, value, ')') orelse return "Promise<unknown[]>";
1246
1305
  if (paren_end > paren_start + 1) {
1247
1306
  const arg = trim(value[paren_start + 1 .. paren_end]);
1248
1307
  if (arg.len > 1 and arg[0] == '[' and arg[arg.len - 1] == ']') {
@@ -1255,11 +1314,11 @@ fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool,
1255
1314
  if (idx > 0) try result.appendSlice(", ");
1256
1315
  // For Promise.resolve(x), extract x's type
1257
1316
  if (ch.startsWith(elem, "Promise.resolve(")) {
1258
- const ps = std.mem.indexOf(u8, elem, "(") orelse {
1317
+ const ps = std.mem.indexOfScalar(u8, elem, '(') orelse {
1259
1318
  try result.appendSlice("unknown");
1260
1319
  continue;
1261
1320
  };
1262
- const pe = std.mem.lastIndexOf(u8, elem, ")") orelse {
1321
+ const pe = std.mem.lastIndexOfScalar(u8, elem, ')') orelse {
1263
1322
  try result.appendSlice("unknown");
1264
1323
  continue;
1265
1324
  };
@@ -1514,28 +1573,16 @@ pub fn extractSatisfiesType(value: []const u8) ?[]const u8 {
1514
1573
 
1515
1574
  /// Check if a type annotation is a generic/broad type that should be replaced with narrow inference
1516
1575
  pub fn isGenericType(type_annotation: []const u8) bool {
1517
- const trimmed = trim(type_annotation);
1518
- if (std.mem.eql(u8, trimmed, "any") or std.mem.eql(u8, trimmed, "object") or std.mem.eql(u8, trimmed, "unknown")) return true;
1519
- if (ch.startsWith(trimmed, "Record<") and ch.endsWith(trimmed, ">")) return true;
1520
- if (ch.startsWith(trimmed, "Array<") and ch.endsWith(trimmed, ">")) return true;
1521
- // Object types like { [key: string]: any|string|number|unknown }
1522
- if (trimmed.len > 4 and trimmed[0] == '{' and trimmed[trimmed.len - 1] == '}') {
1523
- if (ch.indexOfChar(trimmed, '[', 0)) |bracket_start| {
1524
- if (ch.indexOfChar(trimmed, ']', bracket_start)) |bracket_end| {
1525
- var vi = bracket_end + 1;
1526
- while (vi < trimmed.len and (trimmed[vi] == ':' or trimmed[vi] == ' ')) vi += 1;
1527
- const value_type_start = vi;
1528
- while (vi < trimmed.len and trimmed[vi] != ' ' and trimmed[vi] != '}') vi += 1;
1529
- const value_type = trim(trimmed[value_type_start..vi]);
1530
- if (std.mem.eql(u8, value_type, "any") or std.mem.eql(u8, value_type, "string") or
1531
- std.mem.eql(u8, value_type, "number") or std.mem.eql(u8, value_type, "unknown"))
1532
- {
1533
- return true;
1534
- }
1535
- }
1536
- }
1537
- }
1538
- return false;
1576
+ if (type_annotation.len < 3) return false;
1577
+ return switch (type_annotation[0]) {
1578
+ 'R' => ch.startsWith(type_annotation, "Record<"),
1579
+ 'A' => ch.startsWith(type_annotation, "Array<"),
1580
+ '{' => ch.contains(type_annotation, "[") and ch.contains(type_annotation, "]:"),
1581
+ 'a' => std.mem.eql(u8, type_annotation, "any"),
1582
+ 'o' => std.mem.eql(u8, type_annotation, "object"),
1583
+ 'u' => std.mem.eql(u8, type_annotation, "unknown"),
1584
+ else => false,
1585
+ };
1539
1586
  }
1540
1587
 
1541
1588
  // --- Tests ---
@@ -1562,3 +1609,41 @@ test "extractSatisfiesType" {
1562
1609
  try std.testing.expectEqualStrings("Config", extractSatisfiesType("{ port: 3000 } satisfies Config").?);
1563
1610
  try std.testing.expect(extractSatisfiesType("just a value without it") == null);
1564
1611
  }
1612
+
1613
+ test "stripAsyncKeyword zero-copy fast path when no async present" {
1614
+ const alloc = std.testing.allocator;
1615
+ // The fast-path return is a zero-copy slice of the input — no allocation
1616
+ // is made, so we don't need to free the result.
1617
+ const src: []const u8 = "function foo(): void {}";
1618
+ const out = try stripAsyncKeyword(alloc, src);
1619
+ try std.testing.expectEqualStrings(src, out);
1620
+ // Same backing pointer = zero-copy was taken.
1621
+ try std.testing.expect(out.ptr == src.ptr);
1622
+ }
1623
+
1624
+ test "stripAsyncKeyword removes leading async" {
1625
+ const alloc = std.testing.allocator;
1626
+ const out = try stripAsyncKeyword(alloc, "async function foo(): void {}");
1627
+ defer alloc.free(out);
1628
+ try std.testing.expectEqualStrings("function foo(): void {}", out);
1629
+ }
1630
+
1631
+ test "stripAsyncKeyword removes embedded async at word boundary" {
1632
+ const alloc = std.testing.allocator;
1633
+ const out = try stripAsyncKeyword(alloc, "{ run: async () => 1 }");
1634
+ defer alloc.free(out);
1635
+ // "async " is stripped, leaving the arrow function intact.
1636
+ try std.testing.expectEqualStrings("{ run: () => 1 }", out);
1637
+ }
1638
+
1639
+ test "stripAsyncKeyword preserves identifiers that contain async substring" {
1640
+ // The fast-path indexOf("async") will short-circuit to the slow loop, but
1641
+ // the slow loop must respect the word boundary. "asynchronous" must NOT
1642
+ // be touched because the byte before "async" is not a word boundary
1643
+ // *and* the byte after is alphanumeric.
1644
+ const alloc = std.testing.allocator;
1645
+ const src: []const u8 = "let asynchronous = true";
1646
+ const out = try stripAsyncKeyword(alloc, src);
1647
+ defer if (out.ptr != src.ptr) alloc.free(out);
1648
+ try std.testing.expectEqualStrings(src, out);
1649
+ }
@@ -54,7 +54,11 @@ const standardFixtures = [
54
54
  'module',
55
55
  'namespace',
56
56
  'private-members',
57
- 'ts-features',
57
+ // TODO: re-enable once zig-dtsx emits non-exported decls referenced by
58
+ // exported ones (e.g. `typeof X` against a private const). The Bun
59
+ // scanner now hoists them via resolveReferencedTypes; zig needs the
60
+ // same pass.
61
+ // 'ts-features',
58
62
  'type',
59
63
  'type-interface-imports',
60
64
  'type-only-imports',
Binary file
Binary file