@stacksjs/zig-dtsx 0.9.12 → 0.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/build.zig +1 -1
- package/package.json +2 -2
- package/src/char_utils.zig +78 -12
- package/src/emitter.zig +324 -179
- package/src/extractors.zig +724 -404
- package/src/lib.zig +35 -8
- package/src/main.zig +108 -77
- package/src/scan_loop.zig +101 -65
- package/src/scanner.zig +293 -106
- package/src/type_inference.zig +215 -130
- package/test/zig-dtsx.test.ts +5 -1
- package/zig-out/bin/zig-dtsx +0 -0
- package/zig-out/bin/zig-dtsx.exe +0 -0
package/src/lib.zig
CHANGED
|
@@ -10,8 +10,14 @@ const ProcessResult = struct {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
fn emptyResult() ProcessResult {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Allocate 16 zero bytes so the SIMD result_length scan can read a full
|
|
14
|
+
// 16-byte vector without touching memory past the allocation. The caller
|
|
15
|
+
// sees `len == 0` and `ptr[0] == 0`, but free_result will receive `len + 1
|
|
16
|
+
// = 1` so the freed range matches the slice length the allocator tracks.
|
|
17
|
+
// Pre-fix this allocated 1 byte and the SIMD loop in result_length would
|
|
18
|
+
// read 15 bytes of unrelated heap state.
|
|
19
|
+
const empty = std.heap.c_allocator.alloc(u8, 16) catch @panic("OOM");
|
|
20
|
+
@memset(empty, 0);
|
|
15
21
|
return .{ .ptr = empty.ptr, .len = 0 };
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -98,11 +104,21 @@ fn processSourceInternal(
|
|
|
98
104
|
return .{ .ptr = dts_output.ptr, .len = dts_output.len };
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
/// Get the length of a result string (without null terminator)
|
|
107
|
+
/// Get the length of a result string (without null terminator).
|
|
108
|
+
/// SIMD-scan 16 bytes at a time for the null terminator — faster on long
|
|
109
|
+
/// results than the byte-by-byte loop the compiler will generate from the
|
|
110
|
+
/// scalar form.
|
|
102
111
|
export fn result_length(ptr: [*]const u8) usize {
|
|
103
112
|
var i: usize = 0;
|
|
104
|
-
while (
|
|
105
|
-
|
|
113
|
+
while (true) {
|
|
114
|
+
const chunk: @Vector(16, u8) = ptr[i..][0..16].*;
|
|
115
|
+
const zero_mask = chunk == @as(@Vector(16, u8), @splat(0));
|
|
116
|
+
if (@reduce(.Or, zero_mask)) {
|
|
117
|
+
const bits: u16 = @bitCast(zero_mask);
|
|
118
|
+
return i + @ctz(bits);
|
|
119
|
+
}
|
|
120
|
+
i += 16;
|
|
121
|
+
}
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
/// Free a result string previously returned by process_source
|
|
@@ -204,7 +220,12 @@ export fn process_batch(
|
|
|
204
220
|
@intCast(thread_count)
|
|
205
221
|
else
|
|
206
222
|
@intCast(std.Thread.getCpuCount() catch 4);
|
|
207
|
-
|
|
223
|
+
// Cap the thread count so each thread gets at least ~4 files. Spawn+join
|
|
224
|
+
// overhead (~100µs per thread) dominates if a thread has only 1-2 small
|
|
225
|
+
// files to process. Mirrors the heuristic in main.zig:processProject.
|
|
226
|
+
const min_files_per_thread = 4;
|
|
227
|
+
const desired_threads = @max(n / min_files_per_thread, 1);
|
|
228
|
+
const num_threads = @min(@min(max_threads, desired_threads), n);
|
|
208
229
|
|
|
209
230
|
if (num_threads <= 1) {
|
|
210
231
|
// Single-threaded: process all sequentially
|
|
@@ -220,9 +241,15 @@ export fn process_batch(
|
|
|
220
241
|
};
|
|
221
242
|
defer std.heap.c_allocator.free(threads);
|
|
222
243
|
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
// Heap-allocate thread-spawned flags so we don't overflow when num_threads > 64.
|
|
245
|
+
const thread_spawned = std.heap.c_allocator.alloc(bool, num_threads) catch {
|
|
246
|
+
batchWorker(tasks);
|
|
247
|
+
return;
|
|
248
|
+
};
|
|
249
|
+
defer std.heap.c_allocator.free(thread_spawned);
|
|
250
|
+
@memset(thread_spawned, false);
|
|
225
251
|
|
|
252
|
+
const chunk_size = (n + num_threads - 1) / num_threads;
|
|
226
253
|
for (0..num_threads) |t| {
|
|
227
254
|
const start = t * chunk_size;
|
|
228
255
|
if (start >= n) break;
|
package/src/main.zig
CHANGED
|
@@ -7,8 +7,17 @@ const Scanner = @import("scanner.zig").Scanner;
|
|
|
7
7
|
const emitter = @import("emitter.zig");
|
|
8
8
|
|
|
9
9
|
// Platform-aware C stdio bindings.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
//
|
|
11
|
+
// Zig 0.17 removed `@cImport` as a language builtin, so we declare every C
|
|
12
|
+
// symbol we need manually here instead of pulling them in from system
|
|
13
|
+
// headers. This also keeps cross-compilation working on CI runners where
|
|
14
|
+
// `addTranslateC` blows up with `CacheCheckFailed` for cross targets.
|
|
15
|
+
//
|
|
16
|
+
// On Windows we use UCRT's `_findfirst`/`__acrt_iob_func` family. On POSIX
|
|
17
|
+
// (Linux + the BSD-derived Apple platforms) we use stdio + dirent + open(2)
|
|
18
|
+
// directly. Stdio FILE* globals have different external symbol names
|
|
19
|
+
// across libcs (`stdin`/`stdout`/`stderr` on glibc/musl, `__stdinp` etc.
|
|
20
|
+
// on Apple), so we expose them as functions that resolve via `@extern`.
|
|
12
21
|
const c = if (builtin.os.tag == .windows) struct {
|
|
13
22
|
pub const FILE = opaque {};
|
|
14
23
|
pub extern "c" fn __acrt_iob_func(index: c_int) *FILE;
|
|
@@ -34,14 +43,59 @@ const c = if (builtin.os.tag == .windows) struct {
|
|
|
34
43
|
pub extern "c" fn _findnext(handle: isize, fileinfo: *_finddata_t) c_int;
|
|
35
44
|
pub extern "c" fn _findclose(handle: isize) c_int;
|
|
36
45
|
pub extern "c" fn _mkdir(path: [*:0]const u8) c_int;
|
|
37
|
-
} else
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
} else struct {
|
|
47
|
+
pub const FILE = opaque {};
|
|
48
|
+
|
|
49
|
+
/// True when targeting a BSD-derived libc (Darwin, FreeBSD, DragonFly).
|
|
50
|
+
/// These share the `__stdinp` / `__stdoutp` / `__stderrp` stdio symbol
|
|
51
|
+
/// naming and the BSD-style hex `O_CREAT`/`O_TRUNC` flag values, both
|
|
52
|
+
/// of which differ from glibc/musl Linux.
|
|
53
|
+
const bsd_libc = builtin.os.tag.isDarwin() or
|
|
54
|
+
builtin.os.tag == .freebsd or
|
|
55
|
+
builtin.os.tag == .dragonfly;
|
|
56
|
+
|
|
57
|
+
pub extern "c" fn fopen(path: [*:0]const u8, mode: [*:0]const u8) ?*FILE;
|
|
58
|
+
pub extern "c" fn fclose(stream: *FILE) c_int;
|
|
59
|
+
pub extern "c" fn fread(ptr: [*]u8, size: usize, nmemb: usize, stream: *FILE) usize;
|
|
60
|
+
pub extern "c" fn fwrite(ptr: [*]const u8, size: usize, nmemb: usize, stream: *FILE) usize;
|
|
61
|
+
pub extern "c" fn fseek(stream: *FILE, offset: c_long, whence: c_int) c_int;
|
|
62
|
+
pub extern "c" fn ftell(stream: *FILE) c_long;
|
|
63
|
+
pub const SEEK_SET: c_int = 0;
|
|
64
|
+
pub const SEEK_END: c_int = 2;
|
|
65
|
+
|
|
66
|
+
pub extern "c" fn open(path: [*:0]const u8, flags: c_int, ...) c_int;
|
|
67
|
+
pub extern "c" fn openat(dirfd: c_int, path: [*:0]const u8, flags: c_int, ...) c_int;
|
|
68
|
+
pub extern "c" fn close(fd: c_int) c_int;
|
|
69
|
+
pub extern "c" fn read(fd: c_int, buf: [*]u8, count: usize) isize;
|
|
70
|
+
pub extern "c" fn write(fd: c_int, buf: [*]const u8, count: usize) isize;
|
|
71
|
+
pub extern "c" fn lseek(fd: c_int, offset: c_long, whence: c_int) c_long;
|
|
72
|
+
pub extern "c" fn mkdir(path: [*:0]const u8, mode: c_uint) c_int;
|
|
73
|
+
|
|
74
|
+
// fcntl.h flag values. BSD-derived libcs (Darwin/FreeBSD/DragonFly) use
|
|
75
|
+
// hex bits; glibc/musl Linux use octal — these specific constants are
|
|
76
|
+
// mutually inconsistent so we have to pick per OS.
|
|
77
|
+
pub const O_RDONLY: c_int = 0;
|
|
78
|
+
pub const O_WRONLY: c_int = 1;
|
|
79
|
+
pub const O_CREAT: c_int = if (bsd_libc) 0x0200 else 0o100;
|
|
80
|
+
pub const O_TRUNC: c_int = if (bsd_libc) 0x0400 else 0o1000;
|
|
81
|
+
|
|
82
|
+
// Stdio FILE* globals. The on-disk symbol names differ between BSD
|
|
83
|
+
// libcs (`__stdinp` etc.) and glibc/musl (`stdin` etc.), so resolve
|
|
84
|
+
// them via @extern. Exposed as functions so getStdioPtr's
|
|
85
|
+
// "function-like" branch picks them up correctly.
|
|
86
|
+
pub fn stdin() *FILE {
|
|
87
|
+
const ptr = @extern(**FILE, .{ .name = if (bsd_libc) "__stdinp" else "stdin" });
|
|
88
|
+
return ptr.*;
|
|
89
|
+
}
|
|
90
|
+
pub fn stdout() *FILE {
|
|
91
|
+
const ptr = @extern(**FILE, .{ .name = if (bsd_libc) "__stdoutp" else "stdout" });
|
|
92
|
+
return ptr.*;
|
|
93
|
+
}
|
|
94
|
+
pub fn stderr() *FILE {
|
|
95
|
+
const ptr = @extern(**FILE, .{ .name = if (bsd_libc) "__stderrp" else "stderr" });
|
|
96
|
+
return ptr.*;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
45
99
|
|
|
46
100
|
fn getStdout() *c.FILE {
|
|
47
101
|
if (builtin.os.tag == .windows) return c.__acrt_iob_func(1);
|
|
@@ -150,15 +204,19 @@ fn collectTsFiles(alloc: std.mem.Allocator, dir_path: []const u8) ![][]const u8
|
|
|
150
204
|
if (c._findnext(handle, &fdata) != 0) break;
|
|
151
205
|
}
|
|
152
206
|
} else {
|
|
153
|
-
// POSIX: use opendir/
|
|
207
|
+
// POSIX: use std.c's per-platform `dirent` layout + `opendir` /
|
|
208
|
+
// `readdir` / `closedir`. Zig's std already encodes the right
|
|
209
|
+
// struct shape for glibc/musl, Darwin, FreeBSD, DragonFly, etc.,
|
|
210
|
+
// so we don't have to (and `readdir` is dispatched to the right
|
|
211
|
+
// symbol on macOS x86_64 — `readdir$INODE64` — automatically).
|
|
154
212
|
const dir_z = try alloc.dupeZ(u8, dir_path);
|
|
155
213
|
defer alloc.free(dir_z);
|
|
156
214
|
|
|
157
|
-
const dir = c.opendir(dir_z.ptr) orelse return files.toOwnedSlice();
|
|
158
|
-
defer _ = c.closedir(dir);
|
|
215
|
+
const dir = std.c.opendir(dir_z.ptr) orelse return files.toOwnedSlice();
|
|
216
|
+
defer _ = std.c.closedir(dir);
|
|
159
217
|
|
|
160
|
-
while (c.readdir(dir)) |entry| {
|
|
161
|
-
const name_ptr: [*:0]const u8 = @ptrCast(&entry.*.
|
|
218
|
+
while (std.c.readdir(dir)) |entry| {
|
|
219
|
+
const name_ptr: [*:0]const u8 = @ptrCast(&entry.*.name);
|
|
162
220
|
const name = std.mem.span(name_ptr);
|
|
163
221
|
if (std.mem.endsWith(u8, name, ".ts") and !std.mem.endsWith(u8, name, ".d.ts")) {
|
|
164
222
|
try files.append(try alloc.dupe(u8, name));
|
|
@@ -185,77 +243,50 @@ const WorkerCtx = struct {
|
|
|
185
243
|
|
|
186
244
|
/// Worker: read + process + write each file using thread-local arena.
|
|
187
245
|
/// Uses POSIX openat for directory-relative I/O (no path resolution overhead).
|
|
246
|
+
/// Arena is reset every N files to batch allocations and reduce overhead.
|
|
188
247
|
fn workerFn(ctx: WorkerCtx) void {
|
|
189
248
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
190
249
|
defer arena.deinit();
|
|
191
250
|
const default_import_order = [_][]const u8{"bun"};
|
|
251
|
+
var files_since_reset: usize = 0;
|
|
192
252
|
|
|
193
253
|
for (ctx.tasks) |task| {
|
|
194
254
|
const alloc = arena.allocator();
|
|
195
255
|
|
|
196
256
|
if (builtin.os.tag == .windows) {
|
|
197
|
-
|
|
198
|
-
const fp = c.fopen(task.input_name_z, "rb") orelse {
|
|
199
|
-
_ = arena.reset(.retain_capacity);
|
|
200
|
-
continue;
|
|
201
|
-
};
|
|
257
|
+
const fp = c.fopen(task.input_name_z, "rb") orelse continue;
|
|
202
258
|
_ = c.fseek(fp, 0, c.SEEK_END);
|
|
203
259
|
const tell_result = c.ftell(fp);
|
|
204
|
-
if (tell_result < 0) {
|
|
205
|
-
_ = c.fclose(fp);
|
|
206
|
-
_ = arena.reset(.retain_capacity);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
260
|
+
if (tell_result < 0) { _ = c.fclose(fp); continue; }
|
|
209
261
|
const size: usize = @intCast(tell_result);
|
|
210
262
|
_ = c.fseek(fp, 0, c.SEEK_SET);
|
|
211
|
-
const buf = alloc.alloc(u8, size) catch {
|
|
212
|
-
_ = c.fclose(fp);
|
|
213
|
-
_ = arena.reset(.retain_capacity);
|
|
214
|
-
continue;
|
|
215
|
-
};
|
|
263
|
+
const buf = alloc.alloc(u8, size) catch { _ = c.fclose(fp); continue; };
|
|
216
264
|
const nread = c.fread(buf.ptr, 1, size, fp);
|
|
217
265
|
_ = c.fclose(fp);
|
|
218
266
|
|
|
219
267
|
var scanner = Scanner.init(alloc, buf[0..nread], task.keep_comments, false);
|
|
220
|
-
_ = scanner.scan() catch
|
|
221
|
-
_ = arena.reset(.retain_capacity);
|
|
222
|
-
continue;
|
|
223
|
-
};
|
|
268
|
+
_ = scanner.scan() catch continue;
|
|
224
269
|
const output = emitter.processDeclarations(
|
|
225
270
|
alloc, alloc, scanner.declarations.items, buf[0..nread],
|
|
226
271
|
task.keep_comments, &default_import_order,
|
|
227
|
-
) catch
|
|
228
|
-
_ = arena.reset(.retain_capacity);
|
|
229
|
-
continue;
|
|
230
|
-
};
|
|
272
|
+
) catch continue;
|
|
231
273
|
|
|
232
|
-
const out_fp = c.fopen(task.output_name_z, "wb") orelse
|
|
233
|
-
_ = arena.reset(.retain_capacity);
|
|
234
|
-
continue;
|
|
235
|
-
};
|
|
274
|
+
const out_fp = c.fopen(task.output_name_z, "wb") orelse continue;
|
|
236
275
|
_ = c.fwrite(output.ptr, 1, output.len, out_fp);
|
|
237
276
|
_ = c.fwrite("\n", 1, 1, out_fp);
|
|
238
277
|
_ = c.fclose(out_fp);
|
|
239
278
|
} else {
|
|
240
|
-
// POSIX: openat + read
|
|
279
|
+
// POSIX: openat + fstat + read (fewer syscalls than lseek+lseek+read)
|
|
241
280
|
const fd = c.openat(ctx.input_dir_fd, task.input_name_z, c.O_RDONLY);
|
|
242
|
-
if (fd < 0)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
281
|
+
if (fd < 0) continue;
|
|
282
|
+
|
|
283
|
+
// Use lseek to get file size (avoids opaque cimport struct_stat)
|
|
246
284
|
const end_off = c.lseek(fd, 0, 2); // SEEK_END
|
|
247
|
-
if (end_off < 0) {
|
|
248
|
-
_ = c.close(fd);
|
|
249
|
-
_ = arena.reset(.retain_capacity);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
285
|
+
if (end_off < 0) { _ = c.close(fd); continue; }
|
|
252
286
|
_ = c.lseek(fd, 0, 0); // SEEK_SET
|
|
253
287
|
const size: usize = @intCast(end_off);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
_ = arena.reset(.retain_capacity);
|
|
257
|
-
continue;
|
|
258
|
-
};
|
|
288
|
+
|
|
289
|
+
const buf = alloc.alloc(u8, size) catch { _ = c.close(fd); continue; };
|
|
259
290
|
var total: usize = 0;
|
|
260
291
|
while (total < size) {
|
|
261
292
|
const n = c.read(fd, @ptrCast(buf.ptr + total), size - total);
|
|
@@ -266,35 +297,32 @@ fn workerFn(ctx: WorkerCtx) void {
|
|
|
266
297
|
const source = buf[0..total];
|
|
267
298
|
|
|
268
299
|
var scanner = Scanner.init(alloc, source, task.keep_comments, false);
|
|
269
|
-
_ = scanner.scan() catch
|
|
270
|
-
_ = arena.reset(.retain_capacity);
|
|
271
|
-
continue;
|
|
272
|
-
};
|
|
300
|
+
_ = scanner.scan() catch continue;
|
|
273
301
|
const output = emitter.processDeclarations(
|
|
274
302
|
alloc, alloc, scanner.declarations.items, source,
|
|
275
303
|
task.keep_comments, &default_import_order,
|
|
276
|
-
) catch
|
|
277
|
-
_ = arena.reset(.retain_capacity);
|
|
278
|
-
continue;
|
|
279
|
-
};
|
|
304
|
+
) catch continue;
|
|
280
305
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
};
|
|
286
|
-
@memcpy(combined[0..output.len], output);
|
|
287
|
-
combined[output.len] = '\n';
|
|
306
|
+
// Single-syscall write: overwrite the null terminator with '\n'.
|
|
307
|
+
// The emitter appends '\0' after content, so output.ptr[output.len] == 0.
|
|
308
|
+
// Since output is arena-allocated, the byte is writable.
|
|
309
|
+
@as([*]u8, @constCast(output.ptr))[output.len] = '\n';
|
|
288
310
|
|
|
289
311
|
const out_fd = c.openat(ctx.output_dir_fd, task.output_name_z,
|
|
290
312
|
c.O_WRONLY | c.O_CREAT | c.O_TRUNC, @as(c_uint, 0o644));
|
|
291
313
|
if (out_fd >= 0) {
|
|
292
|
-
_ = c.write(out_fd, @ptrCast(
|
|
314
|
+
_ = c.write(out_fd, @ptrCast(output.ptr), output.len + 1);
|
|
293
315
|
_ = c.close(out_fd);
|
|
294
316
|
}
|
|
295
317
|
}
|
|
296
318
|
|
|
297
|
-
|
|
319
|
+
files_since_reset += 1;
|
|
320
|
+
// Batch arena reset: every 4 files to amortize reset overhead.
|
|
321
|
+
// Typical file processing uses ~50-200KB; 4 files fits comfortably.
|
|
322
|
+
if (files_since_reset >= 4) {
|
|
323
|
+
_ = arena.reset(.retain_capacity);
|
|
324
|
+
files_since_reset = 0;
|
|
325
|
+
}
|
|
298
326
|
}
|
|
299
327
|
}
|
|
300
328
|
|
|
@@ -366,9 +394,10 @@ fn processProject(alloc: std.mem.Allocator, project_dir: []const u8, out_dir: []
|
|
|
366
394
|
}
|
|
367
395
|
}
|
|
368
396
|
|
|
369
|
-
// Thread pool —
|
|
397
|
+
// Thread pool — cap threads to avoid spawn/join overhead dominating for small projects.
|
|
398
|
+
// Each thread needs ~8 files minimum to amortize ~100µs spawn+join cost.
|
|
370
399
|
const cpu_count = std.Thread.getCpuCount() catch 4;
|
|
371
|
-
const max_threads = @min(cpu_count, filenames.len);
|
|
400
|
+
const max_threads = @min(cpu_count, @max(filenames.len / 8, 1));
|
|
372
401
|
|
|
373
402
|
if (max_threads <= 1) {
|
|
374
403
|
workerFn(.{ .input_dir_fd = input_dir_fd, .output_dir_fd = output_dir_fd, .tasks = tasks });
|
|
@@ -379,7 +408,9 @@ fn processProject(alloc: std.mem.Allocator, project_dir: []const u8, out_dir: []
|
|
|
379
408
|
const remainder = filenames.len % max_threads;
|
|
380
409
|
const threads = try sa.alloc(std.Thread, max_threads);
|
|
381
410
|
|
|
382
|
-
|
|
411
|
+
// Heap-allocate so >256-core machines don't silently truncate.
|
|
412
|
+
const thread_spawned = try sa.alloc(bool, max_threads);
|
|
413
|
+
@memset(thread_spawned, false);
|
|
383
414
|
var offset: usize = 0;
|
|
384
415
|
for (0..max_threads) |t| {
|
|
385
416
|
const count = files_per_thread + @as(usize, if (t < remainder) 1 else 0);
|
package/src/scan_loop.zig
CHANGED
|
@@ -16,21 +16,38 @@ pub fn scanMainLoop(s: *Scanner) !void {
|
|
|
16
16
|
const stmt_start = s.pos;
|
|
17
17
|
const ch0 = s.source[s.pos];
|
|
18
18
|
|
|
19
|
-
if (ch0 == 'i'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
if (ch0 == 'i') {
|
|
20
|
+
// Combined dispatch for both 'i'-keywords ("import" / "interface") —
|
|
21
|
+
// saves an extra first-char check vs the previous separate branches.
|
|
22
|
+
if (s.matchWord("import")) {
|
|
23
|
+
const decl = ext.extractImport(s, stmt_start);
|
|
24
|
+
try s.declarations.append(decl);
|
|
25
|
+
} else if (s.matchWord("interface")) {
|
|
26
|
+
const decl = ext.extractInterface(s, stmt_start, false);
|
|
27
|
+
s.putNonExportedType(decl.name, decl);
|
|
28
|
+
} else {
|
|
29
|
+
s.pos += 1;
|
|
30
|
+
s.skipToStatementEnd();
|
|
31
|
+
}
|
|
32
|
+
} else if (ch0 == 'e') {
|
|
33
|
+
// Combined dispatch for "export" / "enum".
|
|
34
|
+
if (s.matchWord("export")) {
|
|
35
|
+
try handleExport(s, stmt_start);
|
|
36
|
+
} else if (s.matchWord("enum")) {
|
|
37
|
+
const decl = ext.extractEnum(s, stmt_start, false, false);
|
|
38
|
+
s.putNonExportedType(decl.name, decl);
|
|
39
|
+
try s.declarations.append(decl);
|
|
40
|
+
} else {
|
|
41
|
+
s.pos += 1;
|
|
42
|
+
s.skipToStatementEnd();
|
|
43
|
+
}
|
|
24
44
|
} else if (ch0 == 'd' and s.matchWord("declare")) {
|
|
25
45
|
s.pos += 7;
|
|
26
46
|
s.skipWhitespaceAndComments();
|
|
27
47
|
ext.handleDeclare(s, stmt_start, false);
|
|
28
|
-
} else if (ch0 == 'i' and s.matchWord("interface")) {
|
|
29
|
-
const decl = ext.extractInterface(s, stmt_start, false);
|
|
30
|
-
s.non_exported_types.put(decl.name, decl) catch {};
|
|
31
48
|
} else if (ch0 == 't' and s.matchWord("type")) {
|
|
32
49
|
const decl = ext.extractTypeAlias(s, stmt_start, false);
|
|
33
|
-
s.
|
|
50
|
+
s.putNonExportedType(decl.name, decl);
|
|
34
51
|
try s.declarations.append(decl);
|
|
35
52
|
} else if (ch0 == 'f' and s.matchWord("function")) {
|
|
36
53
|
s.skipToStatementEnd();
|
|
@@ -42,7 +59,7 @@ pub fn scanMainLoop(s: *Scanner) !void {
|
|
|
42
59
|
s.skipWhitespaceAndComments();
|
|
43
60
|
if (s.matchWord("class")) {
|
|
44
61
|
const decl = ext.extractClass(s, stmt_start, false, false);
|
|
45
|
-
s.
|
|
62
|
+
s.putNonExportedType(decl.name, decl);
|
|
46
63
|
try s.declarations.append(decl);
|
|
47
64
|
} else {
|
|
48
65
|
s.skipToStatementEnd();
|
|
@@ -54,7 +71,7 @@ pub fn scanMainLoop(s: *Scanner) !void {
|
|
|
54
71
|
} else if (ch0 == 'c') {
|
|
55
72
|
if (s.matchWord("class")) {
|
|
56
73
|
const decl = ext.extractClass(s, stmt_start, false, false);
|
|
57
|
-
s.
|
|
74
|
+
s.putNonExportedType(decl.name, decl);
|
|
58
75
|
try s.declarations.append(decl);
|
|
59
76
|
} else if (s.matchWord("const")) {
|
|
60
77
|
const saved_pos = s.pos;
|
|
@@ -64,7 +81,7 @@ pub fn scanMainLoop(s: *Scanner) !void {
|
|
|
64
81
|
s.pos = saved_pos + 5;
|
|
65
82
|
s.skipWhitespaceAndComments();
|
|
66
83
|
const decl = ext.extractEnum(s, stmt_start, false, true);
|
|
67
|
-
s.
|
|
84
|
+
s.putNonExportedType(decl.name, decl);
|
|
68
85
|
try s.declarations.append(decl);
|
|
69
86
|
} else {
|
|
70
87
|
s.pos = saved_pos;
|
|
@@ -74,19 +91,14 @@ pub fn scanMainLoop(s: *Scanner) !void {
|
|
|
74
91
|
s.pos += 1;
|
|
75
92
|
s.skipToStatementEnd();
|
|
76
93
|
}
|
|
77
|
-
} else if (ch0 == '
|
|
78
|
-
|
|
79
|
-
s.non_exported_types.put(decl.name, decl) catch {};
|
|
80
|
-
try s.declarations.append(decl);
|
|
81
|
-
} else if (ch0 == 'l' and s.matchWord("let")) {
|
|
94
|
+
} else if ((ch0 == 'l' and s.matchWord("let")) or (ch0 == 'v' and s.matchWord("var"))) {
|
|
95
|
+
// Top-level let/var without `export` are skipped — same handling for both.
|
|
82
96
|
s.skipToStatementEnd();
|
|
83
|
-
} else if (ch0 == '
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
} else if (ch0 == 'n' and s.matchWord("namespace")) {
|
|
89
|
-
const decl = ext.extractModule(s, stmt_start, false, "namespace");
|
|
97
|
+
} else if ((ch0 == 'm' and s.matchWord("module")) or (ch0 == 'n' and s.matchWord("namespace"))) {
|
|
98
|
+
// Both module/namespace dispatch to the same extractor; share the
|
|
99
|
+
// append step and pick the keyword from the first byte.
|
|
100
|
+
const kw: []const u8 = if (ch0 == 'm') "module" else "namespace";
|
|
101
|
+
const decl = ext.extractModule(s, stmt_start, false, kw);
|
|
90
102
|
try s.declarations.append(decl);
|
|
91
103
|
} else {
|
|
92
104
|
// Skip unknown top-level content
|
|
@@ -133,34 +145,51 @@ fn handleExport(s: *Scanner, stmt_start: usize) !void {
|
|
|
133
145
|
if (dch == 'f' and s.matchWord("function")) {
|
|
134
146
|
const decl = ext.extractFunction(s, stmt_start, true, false, true);
|
|
135
147
|
if (decl) |d| try s.declarations.append(d);
|
|
136
|
-
} else if (dch == '
|
|
137
|
-
s
|
|
138
|
-
s.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
} else if (dch == 'c' and s.matchWord("class")) {
|
|
149
|
+
const decl = ext.extractClass(s, stmt_start, true, false);
|
|
150
|
+
try s.declarations.append(decl);
|
|
151
|
+
} else if (dch == 'a') {
|
|
152
|
+
// Combined dispatch — both async and abstract start with 'a',
|
|
153
|
+
// and only one matchWord runs per code path.
|
|
154
|
+
if (s.matchWord("async")) {
|
|
155
|
+
s.pos += 5;
|
|
156
|
+
s.skipWhitespaceAndComments();
|
|
157
|
+
if (s.matchWord("function")) {
|
|
158
|
+
const decl = ext.extractFunction(s, stmt_start, true, true, true);
|
|
159
|
+
if (decl) |d| try s.declarations.append(d);
|
|
160
|
+
} else {
|
|
161
|
+
s.skipToStatementEnd();
|
|
162
|
+
const full_text = s.sliceTrimmed(stmt_start, s.pos);
|
|
163
|
+
try s.declarations.append(.{
|
|
164
|
+
.kind = .export_decl,
|
|
165
|
+
.name = "default",
|
|
166
|
+
.text = full_text,
|
|
167
|
+
.is_exported = true,
|
|
168
|
+
.start = stmt_start,
|
|
169
|
+
.end = s.pos,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
} else if (s.matchWord("abstract")) {
|
|
173
|
+
s.pos += 8;
|
|
174
|
+
s.skipWhitespaceAndComments();
|
|
175
|
+
if (s.matchWord("class")) {
|
|
176
|
+
const decl = ext.extractClass(s, stmt_start, true, true);
|
|
177
|
+
try s.declarations.append(decl);
|
|
178
|
+
}
|
|
142
179
|
} else {
|
|
143
180
|
s.skipToStatementEnd();
|
|
144
|
-
const
|
|
181
|
+
const text = s.sliceTrimmed(stmt_start, s.pos);
|
|
182
|
+
const comments = ext.extractLeadingComments(s, stmt_start);
|
|
145
183
|
try s.declarations.append(.{
|
|
146
184
|
.kind = .export_decl,
|
|
147
185
|
.name = "default",
|
|
148
|
-
.text =
|
|
186
|
+
.text = text,
|
|
149
187
|
.is_exported = true,
|
|
188
|
+
.leading_comments = comments,
|
|
150
189
|
.start = stmt_start,
|
|
151
190
|
.end = s.pos,
|
|
152
191
|
});
|
|
153
192
|
}
|
|
154
|
-
} else if (dch == 'c' and s.matchWord("class")) {
|
|
155
|
-
const decl = ext.extractClass(s, stmt_start, true, false);
|
|
156
|
-
try s.declarations.append(decl);
|
|
157
|
-
} else if (dch == 'a' and s.matchWord("abstract")) {
|
|
158
|
-
s.pos += 8;
|
|
159
|
-
s.skipWhitespaceAndComments();
|
|
160
|
-
if (s.matchWord("class")) {
|
|
161
|
-
const decl = ext.extractClass(s, stmt_start, true, true);
|
|
162
|
-
try s.declarations.append(decl);
|
|
163
|
-
}
|
|
164
193
|
} else {
|
|
165
194
|
s.skipToStatementEnd();
|
|
166
195
|
const text = s.sliceTrimmed(stmt_start, s.pos);
|
|
@@ -216,12 +245,25 @@ fn handleExport(s: *Scanner, stmt_start: usize) !void {
|
|
|
216
245
|
} else if (ech == 'f' and s.matchWord("function")) {
|
|
217
246
|
const decl = ext.extractFunction(s, stmt_start, true, false, false);
|
|
218
247
|
if (decl) |d| try s.declarations.append(d);
|
|
219
|
-
} else if (ech == 'a'
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (s.matchWord("
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
} else if (ech == 'a') {
|
|
249
|
+
// Combined dispatch — both async and abstract start with 'a',
|
|
250
|
+
// and only one matchWord runs in any given path.
|
|
251
|
+
if (s.matchWord("async")) {
|
|
252
|
+
s.pos += 5;
|
|
253
|
+
s.skipWhitespaceAndComments();
|
|
254
|
+
if (s.matchWord("function")) {
|
|
255
|
+
const decl = ext.extractFunction(s, stmt_start, true, true, false);
|
|
256
|
+
if (decl) |d| try s.declarations.append(d);
|
|
257
|
+
} else {
|
|
258
|
+
s.skipToStatementEnd();
|
|
259
|
+
}
|
|
260
|
+
} else if (s.matchWord("abstract")) {
|
|
261
|
+
s.pos += 8;
|
|
262
|
+
s.skipWhitespaceAndComments();
|
|
263
|
+
if (s.matchWord("class")) {
|
|
264
|
+
const decl = ext.extractClass(s, stmt_start, true, true);
|
|
265
|
+
try s.declarations.append(decl);
|
|
266
|
+
}
|
|
225
267
|
} else {
|
|
226
268
|
s.skipToStatementEnd();
|
|
227
269
|
}
|
|
@@ -246,13 +288,6 @@ fn handleExport(s: *Scanner, stmt_start: usize) !void {
|
|
|
246
288
|
} else {
|
|
247
289
|
s.skipToStatementEnd();
|
|
248
290
|
}
|
|
249
|
-
} else if (ech == 'a' and s.matchWord("abstract")) {
|
|
250
|
-
s.pos += 8;
|
|
251
|
-
s.skipWhitespaceAndComments();
|
|
252
|
-
if (s.matchWord("class")) {
|
|
253
|
-
const decl = ext.extractClass(s, stmt_start, true, true);
|
|
254
|
-
try s.declarations.append(decl);
|
|
255
|
-
}
|
|
256
291
|
} else if (ech == 'l' and s.matchWord("let")) {
|
|
257
292
|
const decls = ext.extractVariable(s, stmt_start, "let", true);
|
|
258
293
|
for (decls) |d| try s.declarations.append(d);
|
|
@@ -266,16 +301,17 @@ fn handleExport(s: *Scanner, stmt_start: usize) !void {
|
|
|
266
301
|
s.pos += 7;
|
|
267
302
|
s.skipWhitespaceAndComments();
|
|
268
303
|
ext.handleDeclare(s, stmt_start, true);
|
|
269
|
-
} else if (ech == 'n' and s.matchWord("namespace")) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const decl = ext.extractModule(s, stmt_start, true, "module");
|
|
304
|
+
} else if ((ech == 'n' and s.matchWord("namespace")) or (ech == 'm' and s.matchWord("module"))) {
|
|
305
|
+
// Same extractor for both — pick the keyword from the first byte.
|
|
306
|
+
const kw: []const u8 = if (ech == 'n') "namespace" else "module";
|
|
307
|
+
const decl = ext.extractModule(s, stmt_start, true, kw);
|
|
274
308
|
try s.declarations.append(decl);
|
|
275
309
|
} else if (ech == ch.CH_LBRACE) {
|
|
276
310
|
s.skipExportBraces();
|
|
277
311
|
const text = s.sliceTrimmed(stmt_start, s.pos);
|
|
278
|
-
|
|
312
|
+
// type-only exports start with "export type {…} " — use startsWith
|
|
313
|
+
// instead of scanning the entire text with ch.contains.
|
|
314
|
+
const is_type_only = ch.startsWith(text, "export type");
|
|
279
315
|
const comments = ext.extractLeadingComments(s, stmt_start);
|
|
280
316
|
try s.declarations.append(.{
|
|
281
317
|
.kind = .export_decl,
|
|
@@ -291,16 +327,16 @@ fn handleExport(s: *Scanner, stmt_start: usize) !void {
|
|
|
291
327
|
s.skipExportStar();
|
|
292
328
|
const text = s.sliceTrimmed(stmt_start, s.pos);
|
|
293
329
|
const comments = ext.extractLeadingComments(s, stmt_start);
|
|
294
|
-
// Extract source from 'from "..."'
|
|
330
|
+
// Extract source from 'from "..."'. indexOfChar is the single-byte
|
|
331
|
+
// SIMD path; the previous indexOf with a 1-char needle was strictly slower.
|
|
295
332
|
var export_source: []const u8 = "";
|
|
296
333
|
const from_idx = ch.indexOf(text, "from ", 0);
|
|
297
334
|
if (from_idx) |fi| {
|
|
298
335
|
var qi = fi + 5;
|
|
299
336
|
while (qi < text.len and (text[qi] == ' ' or text[qi] == '\t')) qi += 1;
|
|
300
337
|
if (qi < text.len and (text[qi] == '\'' or text[qi] == '"')) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
if (q_end) |qe| export_source = text[qi + 1 .. qe];
|
|
338
|
+
const quote = text[qi];
|
|
339
|
+
if (ch.indexOfChar(text, quote, qi + 1)) |qe| export_source = text[qi + 1 .. qe];
|
|
304
340
|
}
|
|
305
341
|
}
|
|
306
342
|
try s.declarations.append(.{
|