@stacksjs/zig-dtsx 0.9.10

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.
@@ -0,0 +1,1564 @@
1
+ /// Type inference utilities for DTS generation.
2
+ /// Port of processor/type-inference.ts.
3
+ const std = @import("std");
4
+ const ch = @import("char_utils.zig");
5
+
6
+ const MAX_INFERENCE_DEPTH = 20;
7
+
8
+ /// Error type for type inference operations
9
+ pub const InferError = std.mem.Allocator.Error;
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Module-level storage for computing clean default alongside type inference.
13
+ // This avoids double-parsing: inferObjectType/inferArrayType build the
14
+ // @defaultValue content during the same pass that infers types.
15
+ // ---------------------------------------------------------------------------
16
+ threadlocal var _collect_clean_default: bool = false;
17
+ threadlocal var _clean_default_result: ?[]const u8 = null;
18
+
19
+ /// Enable clean default collection for the next type inference pass.
20
+ /// Must be called before inferNarrowType when you need a @defaultValue.
21
+ pub fn enableCleanDefaultCollection() void {
22
+ _collect_clean_default = true;
23
+ _clean_default_result = null;
24
+ }
25
+
26
+ /// Consume the computed clean default (also disables collection).
27
+ /// Returns null if no clean default was computed.
28
+ pub fn consumeCleanDefault() ?[]const u8 {
29
+ _collect_clean_default = false;
30
+ const val = _clean_default_result;
31
+ _clean_default_result = null;
32
+ return val;
33
+ }
34
+
35
+ /// Check if a string is a numeric literal (matches /^-?\d+(\.\d+)?$/)
36
+ pub fn isNumericLiteral(s: []const u8) bool {
37
+ if (s.len == 0) return false;
38
+ var i: usize = 0;
39
+ if (s[i] == '-') i += 1;
40
+ if (i >= s.len) return false;
41
+ const digit_start = i;
42
+ while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1;
43
+ if (i == digit_start) return false;
44
+ if (i < s.len and s[i] == '.') {
45
+ i += 1;
46
+ const frac_start = i;
47
+ while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1;
48
+ if (i == frac_start) return false;
49
+ }
50
+ return i == s.len;
51
+ }
52
+
53
+ /// Check if s (excluding last char 'n') is all digits — for BigInt literals
54
+ fn isBigIntDigits(s: []const u8) bool {
55
+ if (s.len < 2) return false;
56
+ for (s[0 .. s.len - 1]) |c| {
57
+ if (c < '0' or c > '9') return false;
58
+ }
59
+ return true;
60
+ }
61
+
62
+ /// Trim whitespace from both ends
63
+ fn trim(s: []const u8) []const u8 {
64
+ var start: usize = 0;
65
+ var end: usize = s.len;
66
+ while (start < end and ch.isWhitespace(s[start])) start += 1;
67
+ while (end > start and ch.isWhitespace(s[end - 1])) end -= 1;
68
+ return s[start..end];
69
+ }
70
+
71
+ /// Count occurrences of needle in haystack
72
+ fn countOccurrences(haystack: []const u8, needle: []const u8) usize {
73
+ if (needle.len == 0) return 0;
74
+ var count: usize = 0;
75
+ var pos: usize = 0;
76
+ while (ch.indexOf(haystack, needle, pos)) |idx| {
77
+ count += 1;
78
+ pos = idx + needle.len;
79
+ }
80
+ return count;
81
+ }
82
+
83
+ /// Parse array elements handling nested structures.
84
+ /// Returns slices into the original content string.
85
+ pub fn parseArrayElements(alloc: std.mem.Allocator, content: []const u8) InferError![][]const u8 {
86
+ var elements = std.array_list.Managed([]const u8).init(alloc);
87
+ // Pre-size: estimate element count from top-level commas
88
+ var est: usize = 1;
89
+ for (content) |cc| if (cc == ',') {
90
+ est += 1;
91
+ };
92
+ try elements.ensureTotalCapacity(est);
93
+ var current_start: usize = 0;
94
+ var depth: i32 = 0;
95
+ var in_string = false;
96
+ var string_char: u8 = 0;
97
+ var i: usize = 0;
98
+
99
+ // Skip leading whitespace for current_start
100
+ while (current_start < content.len and ch.isWhitespace(content[current_start])) current_start += 1;
101
+
102
+ while (i < content.len) : (i += 1) {
103
+ const c = content[i];
104
+ const prev = if (i > 0) content[i - 1] else @as(u8, 0);
105
+
106
+ if (!in_string and (c == '"' or c == '\'' or c == '`')) {
107
+ in_string = true;
108
+ string_char = c;
109
+ } else if (in_string and c == string_char and prev != '\\') {
110
+ in_string = false;
111
+ }
112
+
113
+ if (!in_string) {
114
+ if (c == '[' or c == '{' or c == '(') depth += 1;
115
+ if (c == ']' or c == '}' or c == ')') depth -= 1;
116
+
117
+ if (c == ',' and depth == 0) {
118
+ const elem = trim(content[current_start..i]);
119
+ if (elem.len > 0) {
120
+ try elements.append(elem);
121
+ }
122
+ current_start = i + 1;
123
+ while (current_start < content.len and ch.isWhitespace(content[current_start])) current_start += 1;
124
+ continue;
125
+ }
126
+ }
127
+ }
128
+
129
+ // Last element
130
+ const last = trim(content[current_start..content.len]);
131
+ if (last.len > 0) {
132
+ try elements.append(last);
133
+ }
134
+
135
+ return elements.items;
136
+ }
137
+
138
+ /// Clean a method signature: strip async, replace defaults with ?, collapse whitespace.
139
+ /// Single-pass implementation combining all transformations.
140
+ fn cleanMethodSignature(alloc: std.mem.Allocator, signature: []const u8) InferError![]const u8 {
141
+ var input = signature;
142
+ // Remove leading "async "
143
+ if (ch.startsWith(input, "async ")) {
144
+ input = trim(input[5..]);
145
+ }
146
+
147
+ // Fast path: if no async, no defaults (=), no consecutive whitespace, return as-is
148
+ const needs_clean = blk: {
149
+ var prev_ws = false;
150
+ for (input, 0..) |c, i| {
151
+ if (c == '=' and (i + 1 >= input.len or input[i + 1] != '>')) break :blk true;
152
+ const is_ws = ch.isWhitespace(c);
153
+ if (is_ws and prev_ws) break :blk true;
154
+ prev_ws = is_ws;
155
+ if (c == 'a' and i > 0 and !ch.isIdentChar(input[i - 1]) and i + 5 < input.len and
156
+ input[i + 1] == 's' and input[i + 2] == 'y' and
157
+ input[i + 3] == 'n' and input[i + 4] == 'c' and ch.isWhitespace(input[i + 5]))
158
+ break :blk true;
159
+ }
160
+ break :blk false;
161
+ };
162
+ if (!needs_clean) return input;
163
+
164
+ // Single pass: remove async keywords, replace defaults with ?, collapse whitespace
165
+ var buf = std.array_list.Managed(u8).init(alloc);
166
+ try buf.ensureTotalCapacity(input.len);
167
+ var j: usize = 0;
168
+ var in_ws = false;
169
+
170
+ while (j < input.len) {
171
+ const c = input[j];
172
+
173
+ // Skip "async " at word boundaries
174
+ if (j > 0 and !ch.isIdentChar(input[j - 1]) and j + 5 < input.len and
175
+ input[j] == 'a' and input[j + 1] == 's' and input[j + 2] == 'y' and
176
+ input[j + 3] == 'n' and input[j + 4] == 'c' and ch.isWhitespace(input[j + 5]))
177
+ {
178
+ j += 6;
179
+ while (j < input.len and ch.isWhitespace(input[j])) j += 1;
180
+ continue;
181
+ }
182
+
183
+ // Handle identifiers - check for default value patterns (word = value)
184
+ if (ch.isIdentChar(c)) {
185
+ const word_start = j;
186
+ while (j < input.len and ch.isIdentChar(input[j])) j += 1;
187
+ const word_end = j;
188
+
189
+ // Peek past whitespace for '='
190
+ var peek = j;
191
+ while (peek < input.len and ch.isWhitespace(input[peek])) peek += 1;
192
+
193
+ if (peek < input.len and input[peek] == '=' and (peek + 1 >= input.len or input[peek + 1] != '>')) {
194
+ // Default value: skip to , or ) and replace with word?
195
+ var skip = peek + 1;
196
+ while (skip < input.len and input[skip] != ',' and input[skip] != ')') skip += 1;
197
+ // Emit word with collapsed whitespace
198
+ for (input[word_start..word_end]) |wc| {
199
+ if (ch.isWhitespace(wc)) {
200
+ if (!in_ws) { try buf.append(' '); in_ws = true; }
201
+ } else {
202
+ try buf.append(wc); in_ws = false;
203
+ }
204
+ }
205
+ try buf.append('?');
206
+ in_ws = false;
207
+ j = skip;
208
+ continue;
209
+ }
210
+
211
+ // Not a default - emit the word + any whitespace we peeked past
212
+ for (input[word_start..j]) |wc| {
213
+ if (ch.isWhitespace(wc)) {
214
+ if (!in_ws) { try buf.append(' '); in_ws = true; }
215
+ } else {
216
+ try buf.append(wc); in_ws = false;
217
+ }
218
+ }
219
+ continue;
220
+ }
221
+
222
+ // Collapse whitespace
223
+ if (ch.isWhitespace(c)) {
224
+ if (!in_ws) {
225
+ try buf.append(' ');
226
+ in_ws = true;
227
+ }
228
+ j += 1;
229
+ } else {
230
+ try buf.append(c);
231
+ in_ws = false;
232
+ j += 1;
233
+ }
234
+ }
235
+ return trim(buf.items);
236
+ }
237
+
238
+ /// Strip 'async' keywords from a string without collapsing whitespace.
239
+ /// Used when we want to remove async modifiers but preserve multiline formatting.
240
+ fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]const u8 {
241
+ var buf = std.array_list.Managed(u8).init(alloc);
242
+ var j: usize = 0;
243
+ // Remove leading "async "
244
+ if (ch.startsWith(input, "async ")) {
245
+ j = 6;
246
+ // Skip extra whitespace after
247
+ while (j < input.len and input[j] == ' ') j += 1;
248
+ }
249
+ while (j < input.len) {
250
+ if (j > 0 and ch.startsWith(input[j..], "async ")) {
251
+ // Check word boundary: char before must not be alphanumeric or _
252
+ const before = input[j - 1];
253
+ if (!ch.isIdentChar(before)) {
254
+ j += 6; // skip "async "
255
+ // Skip extra spaces (but not newlines) after
256
+ while (j < input.len and input[j] == ' ') j += 1;
257
+ continue;
258
+ }
259
+ }
260
+ try buf.append(input[j]);
261
+ j += 1;
262
+ }
263
+ return buf.items;
264
+ }
265
+
266
+ /// Convert a method definition to a function type.
267
+ /// Input: key = method name (may include generics), value = "(params): ReturnType { body }"
268
+ /// Output: "generics(params) => ReturnType"
269
+ fn convertMethodToFunctionType(alloc: std.mem.Allocator, key: []const u8, method_def: []const u8) InferError![]const u8 {
270
+ var cleaned = method_def;
271
+ // Remove leading async
272
+ if (ch.startsWith(cleaned, "async ")) {
273
+ cleaned = trim(cleaned[5..]);
274
+ }
275
+
276
+ // Extract generics from key (e.g., "onSuccess<T>" -> generics = "<T>")
277
+ var generics: []const u8 = "";
278
+ _ = key; // key is already clean, generics are at start of value if present
279
+ if (cleaned.len > 0 and cleaned[0] == '<') {
280
+ if (findMatchingBracket(cleaned, 0, '<', '>')) |gen_end| {
281
+ generics = cleaned[0 .. gen_end + 1];
282
+ cleaned = trim(cleaned[gen_end + 1 ..]);
283
+ }
284
+ }
285
+
286
+ // Find parameter list
287
+ const param_start = ch.indexOfChar(cleaned, '(', 0) orelse return "() => unknown";
288
+ const param_end = findMatchingBracket(cleaned, param_start, '(', ')') orelse return "() => unknown";
289
+ const params = cleaned[param_start .. param_end + 1];
290
+
291
+ // Extract return type
292
+ var return_type: []const u8 = "unknown";
293
+ const after_params = trim(cleaned[param_end + 1 ..]);
294
+ if (after_params.len > 0 and after_params[0] == ':') {
295
+ // Find return type - everything up to '{' or end
296
+ const type_start: usize = 1; // skip ':'
297
+ var type_end: usize = after_params.len;
298
+ // Look for opening brace (function body)
299
+ var j: usize = type_start;
300
+ var d: i32 = 0;
301
+ while (j < after_params.len) : (j += 1) {
302
+ if (after_params[j] == '<') d += 1 else if (after_params[j] == '>') d -= 1;
303
+ if (d == 0 and after_params[j] == '{') {
304
+ type_end = j;
305
+ break;
306
+ }
307
+ }
308
+ const rt = trim(after_params[type_start..type_end]);
309
+ if (rt.len > 0) return_type = rt;
310
+ }
311
+
312
+ // Clean parameter defaults
313
+ const clean_params = try cleanParameterDefaults(alloc, params);
314
+
315
+ // Build result
316
+ var result = std.array_list.Managed(u8).init(alloc);
317
+ try result.appendSlice(generics);
318
+ try result.appendSlice(clean_params);
319
+ try result.appendSlice(" => ");
320
+ try result.appendSlice(return_type);
321
+ return result.toOwnedSlice();
322
+ }
323
+
324
+ /// Clean parameter defaults: replace `param = value` with `param?`
325
+ fn cleanParameterDefaults(alloc: std.mem.Allocator, params: []const u8) InferError![]const u8 {
326
+ var buf = std.array_list.Managed(u8).init(alloc);
327
+ var j: usize = 0;
328
+ while (j < params.len) {
329
+ // Try to match word= pattern (not =>)
330
+ const word_start = j;
331
+ while (j < params.len and ch.isIdentChar(params[j])) j += 1;
332
+ const word_end = j;
333
+ if (word_end > word_start) {
334
+ // Skip whitespace
335
+ while (j < params.len and ch.isWhitespace(params[j])) j += 1;
336
+ if (j < params.len and params[j] == '=' and (j + 1 >= params.len or params[j + 1] != '>')) {
337
+ // Default value - skip to , or )
338
+ j += 1;
339
+ var d: i32 = 0;
340
+ while (j < params.len) {
341
+ if (params[j] == '(' or params[j] == '[' or params[j] == '{') d += 1;
342
+ if (params[j] == ')' or params[j] == ']' or params[j] == '}') {
343
+ if (d == 0) break;
344
+ d -= 1;
345
+ }
346
+ if (params[j] == ',' and d == 0) break;
347
+ j += 1;
348
+ }
349
+ try buf.appendSlice(params[word_start..word_end]);
350
+ try buf.append('?');
351
+ continue;
352
+ }
353
+ try buf.appendSlice(params[word_start..j]);
354
+ } else {
355
+ try buf.append(params[j]);
356
+ j += 1;
357
+ }
358
+ }
359
+ return buf.items;
360
+ }
361
+
362
+ /// Parse object properties from content between braces.
363
+ /// Returns array of [key, value] pairs as slices into content.
364
+ fn parseObjectProperties(alloc: std.mem.Allocator, content: []const u8) InferError![][2][]const u8 {
365
+ var properties = std.array_list.Managed([2][]const u8).init(alloc);
366
+ // Pre-size: estimate property count from top-level commas
367
+ var est: usize = 1;
368
+ for (content) |cc| if (cc == ',') {
369
+ est += 1;
370
+ };
371
+ try properties.ensureTotalCapacity(est);
372
+ var current_start: usize = 0;
373
+ var key_start: usize = 0;
374
+ var key_end: usize = 0;
375
+ var depth: i32 = 0;
376
+ var in_string = false;
377
+ var string_char: u8 = 0;
378
+ var in_key = true;
379
+ var in_comment = false;
380
+ var is_method = false;
381
+ var i: usize = 0;
382
+
383
+ while (i < content.len) : (i += 1) {
384
+ const c = content[i];
385
+ const prev = if (i > 0) content[i - 1] else @as(u8, 0);
386
+ const next = if (i + 1 < content.len) content[i + 1] else @as(u8, 0);
387
+
388
+ // Track single-line comments — skip to end of line
389
+ 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) {}
392
+ // Update current_start if in key mode so the key slice doesn't include comment text
393
+ if (in_key and i < content.len) {
394
+ current_start = i + 1;
395
+ while (current_start < content.len and ch.isWhitespace(content[current_start])) current_start += 1;
396
+ }
397
+ continue;
398
+ }
399
+
400
+ // Track block comments
401
+ if (!in_string and !in_comment and c == '/' and next == '*') {
402
+ in_comment = true;
403
+ i += 1;
404
+ continue;
405
+ }
406
+ if (in_comment and c == '*' and next == '/') {
407
+ in_comment = false;
408
+ i += 1;
409
+ continue;
410
+ }
411
+ if (in_comment) continue;
412
+
413
+ if (!in_string and (c == '"' or c == '\'' or c == '`')) {
414
+ in_string = true;
415
+ string_char = c;
416
+ } else if (in_string and c == string_char and prev != '\\') {
417
+ in_string = false;
418
+ }
419
+
420
+ if (!in_string) {
421
+ if (c == '(' and depth == 0 and in_key) {
422
+ // Method definition — must check BEFORE general bracket tracking
423
+ key_start = current_start;
424
+ key_end = i;
425
+ current_start = i;
426
+ in_key = false;
427
+ is_method = true;
428
+ depth = 1;
429
+ } else if (c == '{' or c == '[' or c == '(') {
430
+ depth += 1;
431
+ } else if (c == '}' or c == ']' or c == ')') {
432
+ depth -= 1;
433
+ } else if (c == ':' and depth == 0 and in_key) {
434
+ key_start = current_start;
435
+ key_end = i;
436
+ current_start = i + 1;
437
+ in_key = false;
438
+ is_method = false;
439
+ } else if (c == ',' and depth == 0) {
440
+ if (!in_key) {
441
+ var key = trim(content[key_start..key_end]);
442
+ var val = trim(content[current_start..i]);
443
+ if (key.len > 0 and val.len > 0) {
444
+ // Strip async from key if method
445
+ if (is_method and ch.startsWith(key, "async ")) {
446
+ key = trim(key[6..]);
447
+ }
448
+ // Process value based on type - match TS behavior:
449
+ // ANY value starting with '(' goes through convertMethodToFunctionType
450
+ if (val.len > 0 and val[0] == '(') {
451
+ val = try convertMethodToFunctionType(alloc, key, val);
452
+ } else if (ch.contains(val, "=>") or ch.startsWith(val, "function") or ch.startsWith(val, "async")) {
453
+ val = try cleanMethodSignature(alloc, val);
454
+ }
455
+ try properties.append(.{ key, val });
456
+ }
457
+ }
458
+ current_start = i + 1;
459
+ in_key = true;
460
+ is_method = false;
461
+ }
462
+ }
463
+ }
464
+
465
+ // Last property
466
+ if (!in_key) {
467
+ var key = trim(content[key_start..key_end]);
468
+ var val = trim(content[current_start..content.len]);
469
+ if (key.len > 0 and val.len > 0) {
470
+ if (is_method and ch.startsWith(key, "async ")) {
471
+ key = trim(key[6..]);
472
+ }
473
+ if (val.len > 0 and val[0] == '(') {
474
+ val = try convertMethodToFunctionType(alloc, key, val);
475
+ } else if (ch.contains(val, "=>") or ch.startsWith(val, "function") or ch.startsWith(val, "async")) {
476
+ val = try cleanMethodSignature(alloc, val);
477
+ }
478
+ try properties.append(.{ key, val });
479
+ }
480
+ }
481
+
482
+ return properties.items;
483
+ }
484
+
485
+ /// Find matching bracket (open/close) starting from `start`, skipping strings and comments.
486
+ fn findMatchingBracket(str: []const u8, start: usize, open: u8, close: u8) ?usize {
487
+ var depth: i32 = 0;
488
+ var i = start;
489
+ while (i < str.len) : (i += 1) {
490
+ const c = str[i];
491
+ // Skip string literals
492
+ if (c == '"' or c == '\'' or c == '`') {
493
+ i += 1;
494
+ while (i < str.len) : (i += 1) {
495
+ if (str[i] == '\\') {
496
+ i += 1; // skip escaped char
497
+ continue;
498
+ }
499
+ if (str[i] == c) break;
500
+ }
501
+ continue;
502
+ }
503
+ // Skip line comments
504
+ 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) {}
507
+ continue;
508
+ }
509
+ // Skip block comments
510
+ 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
+ }
518
+ continue;
519
+ }
520
+ if (c == open) {
521
+ depth += 1;
522
+ } else if (c == close) {
523
+ depth -= 1;
524
+ if (depth == 0) return i;
525
+ }
526
+ }
527
+ return null;
528
+ }
529
+
530
+ /// Find the main arrow (=>) in a function, ignoring nested arrows
531
+ fn findMainArrowIndex(str: []const u8) ?usize {
532
+ var paren_depth: i32 = 0;
533
+ var bracket_depth: i32 = 0;
534
+ var brace_depth: i32 = 0;
535
+ var angle_depth: i32 = 0;
536
+ var in_string = false;
537
+ var string_char: u8 = 0;
538
+
539
+ var i: usize = 0;
540
+ while (i + 1 < str.len) : (i += 1) {
541
+ const c = str[i];
542
+ const prev = if (i > 0) str[i - 1] else @as(u8, 0);
543
+
544
+ if (in_string) {
545
+ if (c == '\\') {
546
+ i += 1; // skip escaped char
547
+ continue;
548
+ }
549
+ if (c == string_char) in_string = false;
550
+ continue;
551
+ }
552
+
553
+ if (c == '"' or c == '\'' or c == '`') {
554
+ in_string = true;
555
+ string_char = c;
556
+ _ = prev;
557
+ continue;
558
+ }
559
+
560
+ if (c == '(') paren_depth += 1 else if (c == ')') paren_depth -= 1 else if (c == '[') bracket_depth += 1 else if (c == ']') bracket_depth -= 1 else if (c == '{') brace_depth += 1 else if (c == '}') brace_depth -= 1 else if (c == '<') angle_depth += 1 else if (c == '>') {
561
+ if (angle_depth > 0) angle_depth -= 1;
562
+ }
563
+
564
+ if (c == '=' and str[i + 1] == '>' and paren_depth == 0 and bracket_depth == 0 and brace_depth == 0 and angle_depth == 0) {
565
+ return i;
566
+ }
567
+ }
568
+ return null;
569
+ }
570
+
571
+ /// Extract inner function signature from a higher-order function body.
572
+ /// For bodies like "(value: number) => value * factor", extracts "(value: number) => any".
573
+ /// For generic functions where generics include 'T' and inner params include 'T',
574
+ /// uses 'T' as the return type instead of 'any'.
575
+ fn extractInnerFunctionSignature(alloc: std.mem.Allocator, body: []const u8, generics: []const u8) InferError![]const u8 {
576
+ const trimmed_body = trim(body);
577
+ // Match pattern: \s*(params)\s*=>
578
+ if (trimmed_body.len > 0 and trimmed_body[0] == '(') {
579
+ if (findMatchingBracket(trimmed_body, 0, '(', ')')) |paren_end| {
580
+ const inner_params = trim(trimmed_body[1..paren_end]);
581
+ // Check if this is a generic function where T appears in both generics and inner params
582
+ const has_generic_t = generics.len > 0 and ch.contains(generics, "T");
583
+ const inner_has_t = ch.contains(inner_params, "T");
584
+ const inner_return = if (has_generic_t and inner_has_t) "T" else "any";
585
+ var result = std.array_list.Managed(u8).init(alloc);
586
+ try result.appendSlice("(");
587
+ try result.appendSlice(inner_params);
588
+ try result.appendSlice(") => ");
589
+ try result.appendSlice(inner_return);
590
+ return result.toOwnedSlice();
591
+ }
592
+ }
593
+ return "any";
594
+ }
595
+
596
+ /// Single-pass scan hints to avoid multiple ch.contains() calls.
597
+ const ValueHints = struct {
598
+ has_dollar_brace: bool = false, // "${" — template interpolation
599
+ has_arrow: bool = false, // "=>" — arrow function
600
+ has_raw_template: bool = false, // ".raw`" — tagged template literal
601
+
602
+ fn scan(s: []const u8) ValueHints {
603
+ var h = ValueHints{};
604
+ if (s.len < 2) return h;
605
+ var i: usize = 0;
606
+ while (i < s.len - 1) : (i += 1) {
607
+ const c = s[i];
608
+ if (c == '$' and s[i + 1] == '{') { h.has_dollar_brace = true; }
609
+ if (c == '=' and s[i + 1] == '>') { h.has_arrow = true; }
610
+ if (c == '.' and i + 4 < s.len and s[i + 1] == 'r' and s[i + 2] == 'a' and s[i + 3] == 'w' and s[i + 4] == '`') { h.has_raw_template = true; }
611
+ }
612
+ return h;
613
+ }
614
+ };
615
+
616
+ /// Infer narrow type from a value expression.
617
+ /// Returns a type string (allocated from `alloc`).
618
+ pub fn inferNarrowType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, in_union: bool, depth: usize) InferError![]const u8 {
619
+ if (value.len == 0) return "unknown";
620
+ if (depth >= MAX_INFERENCE_DEPTH) return "unknown";
621
+
622
+ const trimmed = trim(value);
623
+ if (trimmed.len == 0) return "unknown";
624
+
625
+ // BigInt expressions
626
+ if (ch.startsWith(trimmed, "BigInt(")) return "bigint";
627
+
628
+ // Symbol.for
629
+ if (ch.startsWith(trimmed, "Symbol.for(")) return "symbol";
630
+
631
+ // Single-pass scan for substring hints (replaces 5 separate ch.contains calls)
632
+ const hints = ValueHints.scan(trimmed);
633
+
634
+ // Tagged template literals
635
+ if (hints.has_raw_template) return "string";
636
+
637
+ // String literals
638
+ if ((trimmed[0] == '"' and trimmed[trimmed.len - 1] == '"') or
639
+ (trimmed[0] == '\'' and trimmed[trimmed.len - 1] == '\'') or
640
+ (trimmed[0] == '`' and trimmed[trimmed.len - 1] == '`'))
641
+ {
642
+ if (!hints.has_dollar_brace) {
643
+ if (!is_const) return "string";
644
+ return trimmed;
645
+ }
646
+ if (is_const) return trimmed;
647
+ return "string";
648
+ }
649
+
650
+ // Number literals
651
+ if (isNumericLiteral(trimmed)) {
652
+ if (!is_const) return "number";
653
+ return trimmed;
654
+ }
655
+
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;
660
+ }
661
+
662
+ // Null and undefined
663
+ if (std.mem.eql(u8, trimmed, "null")) return "null";
664
+ if (std.mem.eql(u8, trimmed, "undefined")) return "undefined";
665
+
666
+ // Array literals
667
+ if (trimmed[0] == '[' and trimmed[trimmed.len - 1] == ']') {
668
+ return inferArrayType(alloc, trimmed, is_const, depth + 1);
669
+ }
670
+
671
+ // Object literals
672
+ if (trimmed[0] == '{' and trimmed[trimmed.len - 1] == '}') {
673
+ return inferObjectType(alloc, trimmed, is_const, depth + 1);
674
+ }
675
+
676
+ // New expressions
677
+ if (ch.startsWith(trimmed, "new ")) {
678
+ return inferNewExpressionType(alloc, trimmed);
679
+ }
680
+
681
+ // Function expressions
682
+ if (hints.has_arrow or ch.startsWith(trimmed, "function") or ch.startsWith(trimmed, "async")) {
683
+ return inferFunctionType(alloc, trimmed, in_union, depth, is_const);
684
+ }
685
+
686
+ // As const assertions
687
+ if (ch.endsWith(trimmed, "as const")) {
688
+ const without_as_const = trim(trimmed[0 .. trimmed.len - 8]);
689
+ if (without_as_const.len > 1 and without_as_const[0] == '[' and without_as_const[without_as_const.len - 1] == ']') {
690
+ const content = trim(without_as_const[1 .. without_as_const.len - 1]);
691
+ if (content.len == 0) return "readonly []";
692
+ const elements = try parseArrayElements(alloc, content);
693
+ var parts = std.array_list.Managed(u8).init(alloc);
694
+ try parts.appendSlice("readonly [");
695
+ for (elements, 0..) |el, idx| {
696
+ if (idx > 0) try parts.appendSlice(", ");
697
+ const el_type = try inferNarrowType(alloc, el, true, false, depth + 1);
698
+ try parts.appendSlice(el_type);
699
+ }
700
+ try parts.append(']');
701
+ return parts.toOwnedSlice();
702
+ }
703
+ return inferNarrowType(alloc, without_as_const, true, in_union, depth + 1);
704
+ }
705
+
706
+ // Template literal
707
+ if (trimmed[0] == '`' and trimmed[trimmed.len - 1] == '`') {
708
+ if (!is_const) return "string";
709
+ if (!hints.has_dollar_brace) return trimmed;
710
+ return "string";
711
+ }
712
+
713
+ // Promise expressions
714
+ if (ch.startsWith(trimmed, "Promise.")) {
715
+ return inferPromiseType(alloc, trimmed, is_const, depth);
716
+ }
717
+
718
+ // Await expressions
719
+ if (ch.startsWith(trimmed, "await ")) return "unknown";
720
+
721
+ // BigInt literals (digits followed by 'n')
722
+ if (trimmed.len > 1 and trimmed[trimmed.len - 1] == 'n' and isBigIntDigits(trimmed)) {
723
+ if (is_const) return trimmed;
724
+ return "bigint";
725
+ }
726
+
727
+ // Symbol
728
+ if (ch.startsWith(trimmed, "Symbol(") or std.mem.eql(u8, trimmed, "Symbol.for")) return "symbol";
729
+
730
+ return "unknown";
731
+ }
732
+
733
+ /// Infer narrow type in union context (widens number/boolean)
734
+ pub fn inferNarrowTypeInUnion(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
735
+ return inferNarrowType(alloc, value, is_const, true, depth);
736
+ }
737
+
738
+ /// Infer array type from array literal
739
+ pub fn inferArrayType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
740
+ const content = trim(value[1 .. value.len - 1]);
741
+ if (content.len == 0) return "never[]";
742
+ if (depth >= MAX_INFERENCE_DEPTH) return "unknown[]";
743
+
744
+ const elements = try parseArrayElements(alloc, content);
745
+
746
+ // Check for 'as const' in any element
747
+ var has_as_const = false;
748
+ for (elements) |el| {
749
+ if (ch.endsWith(trim(el), "as const")) {
750
+ has_as_const = true;
751
+ break;
752
+ }
753
+ }
754
+
755
+ if (has_as_const) {
756
+ var parts = std.array_list.Managed(u8).init(alloc);
757
+ try parts.ensureTotalCapacity(content.len + 32);
758
+ try parts.appendSlice("readonly [\n ");
759
+ for (elements, 0..) |el, idx| {
760
+ if (idx > 0) try parts.appendSlice(" |\n ");
761
+ const trimmed_el = trim(el);
762
+ if (ch.endsWith(trimmed_el, "as const")) {
763
+ const without = trim(trimmed_el[0 .. trimmed_el.len - 8]);
764
+ if (without.len > 1 and without[0] == '[' and without[without.len - 1] == ']') {
765
+ const inner_content = trim(without[1 .. without.len - 1]);
766
+ const inner_elements = try parseArrayElements(alloc, inner_content);
767
+ try parts.appendSlice("readonly [");
768
+ for (inner_elements, 0..) |inner_el, iidx| {
769
+ if (iidx > 0) try parts.appendSlice(", ");
770
+ const t = try inferNarrowType(alloc, inner_el, true, false, depth + 1);
771
+ try parts.appendSlice(t);
772
+ }
773
+ try parts.append(']');
774
+ } else {
775
+ const t = try inferNarrowType(alloc, without, true, false, depth + 1);
776
+ try parts.appendSlice(t);
777
+ }
778
+ } else if (trimmed_el.len > 1 and trimmed_el[0] == '[' and trimmed_el[trimmed_el.len - 1] == ']') {
779
+ const t = try inferArrayType(alloc, trimmed_el, true, depth + 1);
780
+ try parts.appendSlice(t);
781
+ } else {
782
+ const t = try inferNarrowType(alloc, trimmed_el, true, false, depth + 1);
783
+ try parts.appendSlice(t);
784
+ }
785
+ }
786
+ try parts.appendSlice("\n ]");
787
+ return parts.toOwnedSlice();
788
+ }
789
+
790
+ // Regular array processing — also track nested defaults for clean default building
791
+ const track_defaults = _collect_clean_default and !is_const;
792
+ var element_types = std.array_list.Managed([]const u8).init(alloc);
793
+ try element_types.ensureTotalCapacity(elements.len);
794
+ var nested_defaults = std.array_list.Managed(?[]const u8).init(alloc);
795
+ if (track_defaults) try nested_defaults.ensureTotalCapacity(elements.len);
796
+ for (elements) |el| {
797
+ const trimmed_el = trim(el);
798
+ const saved = _clean_default_result;
799
+ _clean_default_result = null;
800
+ if (trimmed_el.len > 1 and trimmed_el[0] == '[' and trimmed_el[trimmed_el.len - 1] == ']') {
801
+ try element_types.append(try inferArrayType(alloc, trimmed_el, is_const, depth + 1));
802
+ } else {
803
+ try element_types.append(try inferNarrowTypeInUnion(alloc, trimmed_el, is_const, depth + 1));
804
+ }
805
+ if (track_defaults) try nested_defaults.append(_clean_default_result);
806
+ _clean_default_result = saved;
807
+ }
808
+
809
+ const types = element_types.items;
810
+
811
+ // Build clean default for non-const arrays (same pass, no re-parse)
812
+ if (track_defaults) {
813
+ if (isSimpleArrayDefault(value)) {
814
+ _clean_default_result = try collapseWhitespace(alloc, value);
815
+ } else {
816
+ var clean_elems = std.array_list.Managed([]const u8).init(alloc);
817
+ try clean_elems.ensureTotalCapacity(elements.len);
818
+ for (elements, 0..) |el, ei| {
819
+ const te = trim(el);
820
+ if (ch.endsWith(te, " as const") or ch.endsWith(te, "as const")) continue;
821
+ if (isPrimitiveLiteral(te) or std.mem.eql(u8, te, "null") or std.mem.eql(u8, te, "undefined")) {
822
+ try clean_elems.append(te);
823
+ } else if (te.len > 0 and te[0] == '[' and isSimpleArrayDefault(te)) {
824
+ try clean_elems.append(try collapseWhitespace(alloc, te));
825
+ } else if (te.len > 0 and te[0] == '{') {
826
+ if (nested_defaults.items[ei]) |nd| try clean_elems.append(nd);
827
+ } else {
828
+ // Re-infer without union context for the clean default
829
+ // (types[ei] was inferred via inferNarrowTypeInUnion which
830
+ // wraps function types in parens and widens return types)
831
+ const clean_type = try inferNarrowType(alloc, te, false, false, 0);
832
+ if (!std.mem.eql(u8, clean_type, "unknown")) {
833
+ try clean_elems.append(clean_type);
834
+ }
835
+ }
836
+ }
837
+ if (clean_elems.items.len > 0) {
838
+ var buf = std.array_list.Managed(u8).init(alloc);
839
+ try buf.append('[');
840
+ for (clean_elems.items, 0..) |item, ci| {
841
+ if (ci > 0) try buf.appendSlice(", ");
842
+ try buf.appendSlice(item);
843
+ }
844
+ try buf.append(']');
845
+ _clean_default_result = try buf.toOwnedSlice();
846
+ }
847
+ }
848
+ }
849
+
850
+ // For const arrays, always create readonly tuples
851
+ if (is_const) {
852
+ var parts = std.array_list.Managed(u8).init(alloc);
853
+ try parts.ensureTotalCapacity(content.len + 16);
854
+ try parts.appendSlice("readonly [");
855
+ for (types, 0..) |t, idx| {
856
+ if (idx > 0) try parts.appendSlice(", ");
857
+ try parts.appendSlice(t);
858
+ }
859
+ try parts.append(']');
860
+ return parts.toOwnedSlice();
861
+ }
862
+
863
+ // Single-pass: deduplicate types (O(1) HashMap lookup) AND check if all are literals
864
+ var unique = std.array_list.Managed([]const u8).init(alloc);
865
+ var unique_set = std.StringHashMap(void).init(alloc);
866
+ try unique_set.ensureTotalCapacity(@intCast(@max(types.len, 4)));
867
+ var all_literals = true;
868
+ for (types) |t| {
869
+ // O(1) dedup check via HashMap
870
+ if (!unique_set.contains(t)) {
871
+ try unique_set.put(t, {});
872
+ try unique.append(t);
873
+ }
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
+ }
882
+ }
883
+
884
+ if (all_literals and types.len <= 10) {
885
+ var parts = std.array_list.Managed(u8).init(alloc);
886
+ try parts.appendSlice("readonly [");
887
+ for (types, 0..) |t, idx| {
888
+ if (idx > 0) try parts.appendSlice(", ");
889
+ try parts.appendSlice(t);
890
+ }
891
+ try parts.append(']');
892
+ return parts.toOwnedSlice();
893
+ }
894
+
895
+ if (unique.items.len == 1) {
896
+ var parts = std.array_list.Managed(u8).init(alloc);
897
+ try parts.appendSlice(unique.items[0]);
898
+ try parts.appendSlice("[]");
899
+ return parts.toOwnedSlice();
900
+ }
901
+
902
+ var parts = std.array_list.Managed(u8).init(alloc);
903
+ try parts.append('(');
904
+ for (unique.items, 0..) |t, idx| {
905
+ if (idx > 0) try parts.appendSlice(" | ");
906
+ try parts.appendSlice(t);
907
+ }
908
+ try parts.appendSlice(")[]");
909
+ return parts.toOwnedSlice();
910
+ }
911
+
912
+ /// Check if a value string is a primitive literal (number, string, boolean)
913
+ 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;
919
+ }
920
+
921
+ /// Check if a type is a base/widened type
922
+ 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");
924
+ }
925
+
926
+ /// Check if an array literal only contains primitives/nested arrays/objects (no runtime expressions)
927
+ fn isSimpleArrayDefault(val: []const u8) bool {
928
+ // Quick scan: reject if it contains runtime keywords or arrow functions
929
+ var i: usize = 0;
930
+ var in_string: bool = false;
931
+ var quote_char: u8 = 0;
932
+ while (i < val.len) : (i += 1) {
933
+ const c = val[i];
934
+ if (in_string) {
935
+ if (c == '\\') {
936
+ i += 1; // skip escaped char
937
+ continue;
938
+ }
939
+ if (c == quote_char) in_string = false;
940
+ continue;
941
+ }
942
+ if (c == '\'' or c == '"' or c == '`') {
943
+ in_string = true;
944
+ quote_char = c;
945
+ continue;
946
+ }
947
+ // Check for arrow =>
948
+ if (c == '=' and i + 1 < val.len and val[i + 1] == '>') return false;
949
+ // Check for keywords: new, console, process, async, await, function, yield
950
+ if (ch.isIdentStart(c)) {
951
+ const start = i;
952
+ while (i < val.len and ch.isIdentChar(val[i])) : (i += 1) {}
953
+ const word = val[start..i];
954
+ // Check what follows the identifier
955
+ var j = i;
956
+ while (j < val.len and ch.isWhitespace(val[j])) : (j += 1) {}
957
+ // If followed by ':', it's an object property key — skip it
958
+ if (j < val.len and val[j] == ':') {
959
+ if (i > 0) i -= 1;
960
+ continue;
961
+ }
962
+ 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;
977
+ if (i > 0) i -= 1; // back up since outer loop will increment
978
+ }
979
+ }
980
+ return true;
981
+ }
982
+
983
+ /// Collapse whitespace in a string to single spaces
984
+ pub fn collapseWhitespace(alloc: std.mem.Allocator, val: []const u8) ![]const u8 {
985
+ // Fast path: check if there's actually any consecutive whitespace or non-space ws
986
+ var needs_collapse = false;
987
+ {
988
+ var prev_ws = false;
989
+ var in_str = false;
990
+ var qc: u8 = 0;
991
+ for (val) |c| {
992
+ if (in_str) {
993
+ if (c == '\\') {
994
+ prev_ws = false;
995
+ continue;
996
+ }
997
+ if (c == qc) in_str = false;
998
+ prev_ws = false;
999
+ continue;
1000
+ }
1001
+ if (c == '\'' or c == '"' or c == '`') {
1002
+ in_str = true;
1003
+ qc = c;
1004
+ prev_ws = false;
1005
+ continue;
1006
+ }
1007
+ if (ch.isWhitespace(c)) {
1008
+ if (c != ' ' or prev_ws) {
1009
+ needs_collapse = true;
1010
+ break;
1011
+ }
1012
+ prev_ws = true;
1013
+ } else {
1014
+ prev_ws = false;
1015
+ }
1016
+ }
1017
+ }
1018
+ if (!needs_collapse) return val;
1019
+
1020
+ // Slow path: actually collapse
1021
+ var result = std.array_list.Managed(u8).init(alloc);
1022
+ try result.ensureTotalCapacity(val.len);
1023
+ var in_ws = false;
1024
+ var in_string = false;
1025
+ var quote_char: u8 = 0;
1026
+ for (val) |c| {
1027
+ if (in_string) {
1028
+ try result.append(c);
1029
+ if (c == '\\') {
1030
+ // next char is escaped, handled on next iteration
1031
+ } else if (c == quote_char) {
1032
+ in_string = false;
1033
+ }
1034
+ continue;
1035
+ }
1036
+ if (c == '\'' or c == '"' or c == '`') {
1037
+ in_string = true;
1038
+ quote_char = c;
1039
+ in_ws = false;
1040
+ try result.append(c);
1041
+ continue;
1042
+ }
1043
+ if (ch.isWhitespace(c)) {
1044
+ if (!in_ws) {
1045
+ try result.append(' ');
1046
+ in_ws = true;
1047
+ }
1048
+ } else {
1049
+ in_ws = false;
1050
+ try result.append(c);
1051
+ }
1052
+ }
1053
+ return result.toOwnedSlice();
1054
+ }
1055
+
1056
+
1057
+ /// Infer object type from object literal.
1058
+ /// When _collect_clean_default is set and !is_const, also builds the @defaultValue
1059
+ /// content during the same pass — avoiding double-parsing of parseObjectProperties.
1060
+ pub fn inferObjectType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
1061
+ const content = trim(value[1 .. value.len - 1]);
1062
+ if (content.len == 0) return "{}";
1063
+ if (depth >= MAX_INFERENCE_DEPTH) return "Record<string, unknown>";
1064
+
1065
+ const properties = try parseObjectProperties(alloc, content);
1066
+
1067
+ // Track clean default parts when collecting and this is a non-const container
1068
+ const build_default = _collect_clean_default and !is_const;
1069
+ var clean_props = std.array_list.Managed([]const u8).init(alloc);
1070
+
1071
+ var parts = std.array_list.Managed(u8).init(alloc);
1072
+ try parts.ensureTotalCapacity(content.len + 32);
1073
+ try parts.appendSlice("{\n ");
1074
+ for (properties, 0..) |prop, idx| {
1075
+ if (idx > 0) try parts.appendSlice(";\n ");
1076
+
1077
+ // Save parent's clean default before recursive call (nested objects overwrite it)
1078
+ const saved_default = _clean_default_result;
1079
+ _clean_default_result = null;
1080
+
1081
+ var val_type = try inferNarrowType(alloc, prop[1], is_const, false, depth + 1);
1082
+
1083
+ // Capture nested clean default (set by recursive inferObjectType/inferArrayType)
1084
+ const nested_default = _clean_default_result;
1085
+ _clean_default_result = saved_default; // restore parent's
1086
+
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);
1092
+ }
1093
+
1094
+ // Add inline @defaultValue for widened primitive properties
1095
+ const raw_val = trim(prop[1]);
1096
+ if (!is_const and isBaseType(val_type) and isPrimitiveLiteral(raw_val)) {
1097
+ try parts.appendSlice("/** @defaultValue ");
1098
+ try parts.appendSlice(raw_val);
1099
+ try parts.appendSlice(" */\n ");
1100
+ }
1101
+ try parts.appendSlice(prop[0]); // key
1102
+ try parts.appendSlice(": ");
1103
+ try parts.appendSlice(val_type);
1104
+
1105
+ // Build clean default entry for this property (same loop, no re-parse)
1106
+ if (build_default) {
1107
+ if (ch.endsWith(raw_val, " as const") or ch.endsWith(raw_val, "as const")) {
1108
+ // skip — type already narrow
1109
+ } else if (isPrimitiveLiteral(raw_val)) {
1110
+ var ps = std.array_list.Managed(u8).init(alloc);
1111
+ try ps.appendSlice(prop[0]);
1112
+ try ps.appendSlice(": ");
1113
+ try ps.appendSlice(raw_val);
1114
+ try clean_props.append(try ps.toOwnedSlice());
1115
+ } else if (raw_val.len > 0 and raw_val[0] == '[' and isSimpleArrayDefault(raw_val)) {
1116
+ var ps = std.array_list.Managed(u8).init(alloc);
1117
+ try ps.appendSlice(prop[0]);
1118
+ try ps.appendSlice(": ");
1119
+ try ps.appendSlice(try collapseWhitespace(alloc, raw_val));
1120
+ try clean_props.append(try ps.toOwnedSlice());
1121
+ } else if (raw_val.len > 0 and raw_val[0] == '{') {
1122
+ if (nested_default) |nd| {
1123
+ var ps = std.array_list.Managed(u8).init(alloc);
1124
+ try ps.appendSlice(prop[0]);
1125
+ try ps.appendSlice(": ");
1126
+ try ps.appendSlice(nd);
1127
+ try clean_props.append(try ps.toOwnedSlice());
1128
+ }
1129
+ } else if (raw_val.len > 0 and raw_val[0] != '[' and
1130
+ (ch.contains(raw_val, "=>") or ch.startsWith(raw_val, "function") or ch.startsWith(raw_val, "async")))
1131
+ {
1132
+ // Use already-computed val_type instead of re-inferring
1133
+ var ps = std.array_list.Managed(u8).init(alloc);
1134
+ try ps.appendSlice(prop[0]);
1135
+ try ps.appendSlice(": ");
1136
+ try ps.appendSlice(val_type);
1137
+ try clean_props.append(try ps.toOwnedSlice());
1138
+ }
1139
+ }
1140
+ }
1141
+ try parts.appendSlice("\n}");
1142
+
1143
+ // Store computed clean default for parent/emitter to consume
1144
+ if (build_default and clean_props.items.len > 0) {
1145
+ var one_line = std.array_list.Managed(u8).init(alloc);
1146
+ try one_line.appendSlice("{ ");
1147
+ for (clean_props.items, 0..) |item, ci| {
1148
+ if (ci > 0) try one_line.appendSlice(", ");
1149
+ try one_line.appendSlice(item);
1150
+ }
1151
+ try one_line.appendSlice(" }");
1152
+ const one_line_str = try one_line.toOwnedSlice();
1153
+ if (one_line_str.len <= 80) {
1154
+ _clean_default_result = one_line_str;
1155
+ } else {
1156
+ // Multi-line with proper indentation based on nesting depth.
1157
+ // depth increments by 2 per nesting level (once in inferNarrowType, once here),
1158
+ // so indent = (depth - 1) / 2 maps depth to the correct indent level.
1159
+ const indent = if (depth > 0) (depth - 1) / 2 else 0;
1160
+ const pad_size = (indent + 1) * 2;
1161
+ const close_pad_size = indent * 2;
1162
+ var ml = std.array_list.Managed(u8).init(alloc);
1163
+ try ml.appendSlice("{\n");
1164
+ 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(' ');
1176
+ }
1177
+ try ml.append('}');
1178
+ _clean_default_result = try ml.toOwnedSlice();
1179
+ }
1180
+ }
1181
+
1182
+ return parts.toOwnedSlice();
1183
+ }
1184
+
1185
+ /// Infer type from new expression
1186
+ fn inferNewExpressionType(alloc: std.mem.Allocator, value: []const u8) InferError![]const u8 {
1187
+ // Extract class name after "new "
1188
+ var i: usize = 4; // skip "new "
1189
+ while (i < value.len and ch.isWhitespace(value[i])) i += 1;
1190
+ const name_start = i;
1191
+
1192
+ // Read class name (must start with uppercase)
1193
+ if (i >= value.len or value[i] < 'A' or value[i] > 'Z') return "unknown";
1194
+ while (i < value.len and ch.isIdentChar(value[i])) i += 1;
1195
+ const class_name = value[name_start..i];
1196
+
1197
+ // Check for explicit generic type parameters
1198
+ if (i < value.len and value[i] == '<') {
1199
+ if (findMatchingBracket(value, i, '<', '>')) |end| {
1200
+ var result = std.array_list.Managed(u8).init(alloc);
1201
+ try result.appendSlice(class_name);
1202
+ try result.appendSlice(value[i .. end + 1]);
1203
+ return result.toOwnedSlice();
1204
+ }
1205
+ }
1206
+
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;
1221
+ }
1222
+
1223
+ /// Infer type from Promise expression
1224
+ fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
1225
+ 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>";
1229
+ if (paren_end > paren_start + 1) {
1230
+ const arg = trim(value[paren_start + 1 .. paren_end]);
1231
+ // Promise resolved values are immutable, so preserve is_const from context
1232
+ const arg_type = try inferNarrowType(alloc, arg, is_const, false, depth + 1);
1233
+ var result = std.array_list.Managed(u8).init(alloc);
1234
+ try result.appendSlice("Promise<");
1235
+ try result.appendSlice(arg_type);
1236
+ try result.append('>');
1237
+ return result.toOwnedSlice();
1238
+ }
1239
+ return "Promise<unknown>";
1240
+ }
1241
+ if (ch.startsWith(value, "Promise.reject(")) return "Promise<never>";
1242
+ if (ch.startsWith(value, "Promise.all(")) {
1243
+ // 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[]>";
1246
+ if (paren_end > paren_start + 1) {
1247
+ const arg = trim(value[paren_start + 1 .. paren_end]);
1248
+ if (arg.len > 1 and arg[0] == '[' and arg[arg.len - 1] == ']') {
1249
+ // It's an array argument — infer as tuple
1250
+ const elements = try parseArrayElements(alloc, arg[1 .. arg.len - 1]);
1251
+ if (elements.len > 0) {
1252
+ var result = std.array_list.Managed(u8).init(alloc);
1253
+ try result.appendSlice("Promise<[");
1254
+ for (elements, 0..) |elem, idx| {
1255
+ if (idx > 0) try result.appendSlice(", ");
1256
+ // For Promise.resolve(x), extract x's type
1257
+ if (ch.startsWith(elem, "Promise.resolve(")) {
1258
+ const ps = std.mem.indexOf(u8, elem, "(") orelse {
1259
+ try result.appendSlice("unknown");
1260
+ continue;
1261
+ };
1262
+ const pe = std.mem.lastIndexOf(u8, elem, ")") orelse {
1263
+ try result.appendSlice("unknown");
1264
+ continue;
1265
+ };
1266
+ if (pe > ps + 1) {
1267
+ const inner_arg = trim(elem[ps + 1 .. pe]);
1268
+ const inner_type = try inferNarrowType(alloc, inner_arg, is_const, false, depth + 1);
1269
+ try result.appendSlice(inner_type);
1270
+ } else {
1271
+ try result.appendSlice("unknown");
1272
+ }
1273
+ } else {
1274
+ const elem_type = try inferNarrowType(alloc, elem, is_const, false, depth + 1);
1275
+ try result.appendSlice(elem_type);
1276
+ }
1277
+ }
1278
+ try result.appendSlice("]>");
1279
+ return result.toOwnedSlice();
1280
+ }
1281
+ }
1282
+ }
1283
+ return "Promise<unknown[]>";
1284
+ }
1285
+
1286
+ return "Promise<unknown>";
1287
+ }
1288
+
1289
+ /// Infer function type from function expression
1290
+ pub fn inferFunctionType(alloc: std.mem.Allocator, value: []const u8, in_union: bool, depth: usize, is_const: bool) InferError![]const u8 {
1291
+ const trimmed = trim(value);
1292
+
1293
+ // Handle very complex function types early
1294
+ if (trimmed.len > 200 and countOccurrences(trimmed, "=>") > 2 and countOccurrences(trimmed, "<") > 5 and !ch.startsWith(trimmed, "function")) {
1295
+ const func_type = "(...args: any[]) => any";
1296
+ if (in_union) {
1297
+ var result = std.array_list.Managed(u8).init(alloc);
1298
+ try result.append('(');
1299
+ try result.appendSlice(func_type);
1300
+ try result.append(')');
1301
+ return result.toOwnedSlice();
1302
+ }
1303
+ return func_type;
1304
+ }
1305
+
1306
+ // Handle async arrow functions
1307
+ if (ch.startsWith(trimmed, "async ") and ch.contains(trimmed, "=>")) {
1308
+ const async_removed = trim(trimmed[5..]);
1309
+ if (findMainArrowIndex(async_removed)) |arrow_idx| {
1310
+ var params = trim(async_removed[0..arrow_idx]);
1311
+ const body = trim(async_removed[arrow_idx + 2 ..]);
1312
+
1313
+ // Wrap bare params
1314
+ if (params.len == 0 or std.mem.eql(u8, params, "()")) {
1315
+ params = "()";
1316
+ } else if (params[0] != '(') {
1317
+ var p = std.array_list.Managed(u8).init(alloc);
1318
+ try p.append('(');
1319
+ try p.appendSlice(params);
1320
+ try p.append(')');
1321
+ params = try p.toOwnedSlice();
1322
+ }
1323
+
1324
+ var return_type: []const u8 = "unknown";
1325
+ if (body.len > 0 and body[0] != '{') {
1326
+ return_type = try inferNarrowType(alloc, body, is_const, false, depth + 1);
1327
+ }
1328
+
1329
+ var result = std.array_list.Managed(u8).init(alloc);
1330
+ try result.appendSlice(params);
1331
+ try result.appendSlice(" => Promise<");
1332
+ try result.appendSlice(return_type);
1333
+ try result.append('>');
1334
+ const func_type = try result.toOwnedSlice();
1335
+
1336
+ if (in_union) {
1337
+ var wrapped = std.array_list.Managed(u8).init(alloc);
1338
+ try wrapped.append('(');
1339
+ try wrapped.appendSlice(func_type);
1340
+ try wrapped.append(')');
1341
+ return wrapped.toOwnedSlice();
1342
+ }
1343
+ return func_type;
1344
+ }
1345
+ }
1346
+
1347
+ // Regular arrow functions
1348
+ if (ch.contains(trimmed, "=>")) {
1349
+ var generics: []const u8 = "";
1350
+ var remaining = trimmed;
1351
+
1352
+ if (trimmed[0] == '<') {
1353
+ if (findMatchingBracket(trimmed, 0, '<', '>')) |gen_end| {
1354
+ generics = trimmed[0 .. gen_end + 1];
1355
+ remaining = trim(trimmed[gen_end + 1 ..]);
1356
+ }
1357
+ }
1358
+
1359
+ if (findMainArrowIndex(remaining)) |arrow_idx| {
1360
+ var params = trim(remaining[0..arrow_idx]);
1361
+ const body = trim(remaining[arrow_idx + 2 ..]);
1362
+
1363
+ // Check for explicit return type annotation
1364
+ var explicit_return_type: []const u8 = "";
1365
+ // Look for ): ReturnType pattern at end of params
1366
+ if (std.mem.lastIndexOf(u8, params, "):")) |ri| {
1367
+ explicit_return_type = trim(params[ri + 2 ..]);
1368
+ params = params[0 .. ri + 1];
1369
+ }
1370
+
1371
+ if (params.len == 0 or std.mem.eql(u8, params, "()")) {
1372
+ params = "()";
1373
+ } else if (params[0] != '(') {
1374
+ var p = std.array_list.Managed(u8).init(alloc);
1375
+ try p.append('(');
1376
+ try p.appendSlice(params);
1377
+ try p.append(')');
1378
+ params = try p.toOwnedSlice();
1379
+ }
1380
+
1381
+ var return_type: []const u8 = "unknown";
1382
+ if (explicit_return_type.len > 0) {
1383
+ return_type = explicit_return_type;
1384
+ } else if (body.len > 0 and body[0] == '{') {
1385
+ return_type = "unknown";
1386
+ } else if (ch.contains(body, "=>")) {
1387
+ // Higher-order function returning another function
1388
+ // Try to extract the outer function signature: (params) =>
1389
+ const inner = try extractInnerFunctionSignature(alloc, body, generics);
1390
+ return_type = inner;
1391
+ } else if (!in_union) {
1392
+ return_type = try inferNarrowType(alloc, body, is_const, false, depth + 1);
1393
+ }
1394
+
1395
+ var result = std.array_list.Managed(u8).init(alloc);
1396
+ try result.appendSlice(generics);
1397
+ try result.appendSlice(params);
1398
+ try result.appendSlice(" => ");
1399
+ try result.appendSlice(return_type);
1400
+ const func_type = try result.toOwnedSlice();
1401
+
1402
+ if (in_union) {
1403
+ var wrapped = std.array_list.Managed(u8).init(alloc);
1404
+ try wrapped.append('(');
1405
+ try wrapped.appendSlice(func_type);
1406
+ try wrapped.append(')');
1407
+ return wrapped.toOwnedSlice();
1408
+ }
1409
+ return func_type;
1410
+ }
1411
+
1412
+ const fallback = "() => unknown";
1413
+ if (in_union) {
1414
+ var result = std.array_list.Managed(u8).init(alloc);
1415
+ try result.append('(');
1416
+ try result.appendSlice(fallback);
1417
+ try result.append(')');
1418
+ return result.toOwnedSlice();
1419
+ }
1420
+ return fallback;
1421
+ }
1422
+
1423
+ // function expressions
1424
+ if (ch.startsWith(trimmed, "function")) {
1425
+ // Try to extract params
1426
+ if (ch.indexOfChar(trimmed, '(', 0)) |paren_start| {
1427
+ if (findMatchingBracket(trimmed, paren_start, '(', ')')) |paren_end| {
1428
+ const params = trim(trimmed[paren_start .. paren_end + 1]);
1429
+ // Check for generator
1430
+ const is_generator = ch.indexOfChar(trimmed[0..paren_start], '*', 0) != null;
1431
+ // Check for generics
1432
+ var generics: []const u8 = "";
1433
+ if (ch.indexOfChar(trimmed, '<', 0)) |angle_start| {
1434
+ if (angle_start < paren_start) {
1435
+ if (findMatchingBracket(trimmed, angle_start, '<', '>')) |angle_end| {
1436
+ generics = trimmed[angle_start .. angle_end + 1];
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ // Check for explicit return type annotation after params
1442
+ var return_type: []const u8 = if (is_generator) "Generator<any, any, any>" else "unknown";
1443
+ const after_params = trim(trimmed[paren_end + 1 ..]);
1444
+ if (after_params.len > 0 and after_params[0] == ':') {
1445
+ // Extract return type up to '{'
1446
+ var rt_end: usize = after_params.len;
1447
+ var rt_depth: i32 = 0;
1448
+ var rt_i: usize = 1;
1449
+ while (rt_i < after_params.len) : (rt_i += 1) {
1450
+ if (after_params[rt_i] == '<') rt_depth += 1 else if (after_params[rt_i] == '>') rt_depth -= 1;
1451
+ if (rt_depth == 0 and after_params[rt_i] == '{') {
1452
+ rt_end = rt_i;
1453
+ break;
1454
+ }
1455
+ }
1456
+ const rt = trim(after_params[1..rt_end]);
1457
+ if (rt.len > 0) return_type = rt;
1458
+ }
1459
+
1460
+ var result = std.array_list.Managed(u8).init(alloc);
1461
+ if (in_union) try result.append('(');
1462
+ try result.appendSlice(generics);
1463
+ try result.appendSlice(params);
1464
+ try result.appendSlice(" => ");
1465
+ try result.appendSlice(return_type);
1466
+ if (in_union) try result.append(')');
1467
+ return result.toOwnedSlice();
1468
+ }
1469
+ }
1470
+
1471
+ const fallback = "(...args: any[]) => unknown";
1472
+ if (in_union) {
1473
+ var result = std.array_list.Managed(u8).init(alloc);
1474
+ try result.append('(');
1475
+ try result.appendSlice(fallback);
1476
+ try result.append(')');
1477
+ return result.toOwnedSlice();
1478
+ }
1479
+ return fallback;
1480
+ }
1481
+
1482
+ const fallback = "() => unknown";
1483
+ if (in_union) {
1484
+ var result = std.array_list.Managed(u8).init(alloc);
1485
+ try result.append('(');
1486
+ try result.appendSlice(fallback);
1487
+ try result.append(')');
1488
+ return result.toOwnedSlice();
1489
+ }
1490
+ return fallback;
1491
+ }
1492
+
1493
+ /// Extract type from 'satisfies' operator
1494
+ pub fn extractSatisfiesType(value: []const u8) ?[]const u8 {
1495
+ const needle = " satisfies ";
1496
+ // Find last occurrence
1497
+ var last_idx: ?usize = null;
1498
+ var search_from: usize = 0;
1499
+ while (ch.indexOf(value, needle, search_from)) |idx| {
1500
+ last_idx = idx;
1501
+ search_from = idx + 1;
1502
+ }
1503
+
1504
+ if (last_idx) |si| {
1505
+ var type_str = trim(value[si + needle.len ..]);
1506
+ // Remove trailing semicolon
1507
+ if (type_str.len > 0 and type_str[type_str.len - 1] == ';') {
1508
+ type_str = trim(type_str[0 .. type_str.len - 1]);
1509
+ }
1510
+ if (type_str.len > 0) return type_str;
1511
+ }
1512
+ return null;
1513
+ }
1514
+
1515
+ /// Check if a type annotation is a generic/broad type that should be replaced with narrow inference
1516
+ 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;
1539
+ }
1540
+
1541
+ // --- Tests ---
1542
+ test "isNumericLiteral" {
1543
+ try std.testing.expect(isNumericLiteral("42"));
1544
+ try std.testing.expect(isNumericLiteral("-3.14"));
1545
+ try std.testing.expect(isNumericLiteral("0"));
1546
+ try std.testing.expect(!isNumericLiteral(""));
1547
+ try std.testing.expect(!isNumericLiteral("abc"));
1548
+ try std.testing.expect(!isNumericLiteral("-"));
1549
+ try std.testing.expect(!isNumericLiteral("3."));
1550
+ }
1551
+
1552
+ test "inferNarrowType basics" {
1553
+ const alloc = std.testing.allocator;
1554
+ try std.testing.expectEqualStrings("42", try inferNarrowType(alloc, "42", true, false, 0));
1555
+ try std.testing.expectEqualStrings("number", try inferNarrowType(alloc, "42", false, true, 0));
1556
+ try std.testing.expectEqualStrings("true", try inferNarrowType(alloc, "true", true, false, 0));
1557
+ try std.testing.expectEqualStrings("null", try inferNarrowType(alloc, "null", false, false, 0));
1558
+ try std.testing.expectEqualStrings("unknown", try inferNarrowType(alloc, "", false, false, 0));
1559
+ }
1560
+
1561
+ test "extractSatisfiesType" {
1562
+ try std.testing.expectEqualStrings("Config", extractSatisfiesType("{ port: 3000 } satisfies Config").?);
1563
+ try std.testing.expect(extractSatisfiesType("just a value without it") == null);
1564
+ }