Files
bun.sh/src/semver/SemverQuery.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

794 lines
26 KiB
Zig

const Query = @This();
/// Linked-list of AND ranges
/// "^1 ^2"
/// ----|-----
/// That is two Query
pub const Op = enum {
none,
AND,
OR,
};
range: Range = Range{},
// AND
next: ?*Query = null,
const Formatter = struct {
query: *const Query,
buffer: []const u8,
pub fn format(formatter: Formatter, writer: *std.Io.Writer) !void {
const this = formatter.query;
if (this.next) |ptr| {
if (ptr.range.hasLeft() or ptr.range.hasRight()) {
try writer.print("{f} && {f}", .{ this.range.fmt(formatter.buffer), ptr.range.fmt(formatter.buffer) });
return;
}
}
try writer.print("{f}", .{this.range.fmt(formatter.buffer)});
}
};
pub fn fmt(this: *const Query, buf: []const u8) @This().Formatter {
return .{ .query = this, .buffer = buf };
}
/// Linked-list of Queries OR'd together
/// "^1 || ^2"
/// ----|-----
/// That is two List
pub const List = struct {
head: Query = Query{},
tail: ?*Query = null,
// OR
next: ?*List = null,
const Formatter = struct {
list: *const List,
buffer: []const u8,
pub fn format(formatter: @This(), writer: *std.Io.Writer) !void {
const this = formatter.list;
if (this.next) |ptr| {
try writer.print("{f} || {f}", .{ this.head.fmt(formatter.buffer), ptr.fmt(formatter.buffer) });
} else {
try writer.print("{f}", .{this.head.fmt(formatter.buffer)});
}
}
};
pub fn fmt(this: *const List, buf: []const u8) @This().Formatter {
return .{ .list = this, .buffer = buf };
}
pub fn satisfies(list: *const List, version: Version, list_buf: string, version_buf: string) bool {
return list.head.satisfies(
version,
list_buf,
version_buf,
) or (list.next orelse return false).satisfies(
version,
list_buf,
version_buf,
);
}
pub fn satisfiesPre(list: *const List, version: Version, list_buf: string, version_buf: string) bool {
if (comptime Environment.allow_assert) {
assert(version.tag.hasPre());
}
// `version` has a prerelease tag:
// - needs to satisfy each comparator in the query (<comparator> AND <comparator> AND ...) like normal comparison
// - if it does, also needs to match major, minor, patch with at least one of the other versions
// with a prerelease
// https://github.com/npm/node-semver/blob/ac9b35769ab0ddfefd5a3af4a3ecaf3da2012352/classes/range.js#L505
var pre_matched = false;
return (list.head.satisfiesPre(
version,
list_buf,
version_buf,
&pre_matched,
) and pre_matched) or (list.next orelse return false).satisfiesPre(
version,
list_buf,
version_buf,
);
}
pub fn eql(lhs: *const List, rhs: *const List) bool {
if (!lhs.head.eql(&rhs.head)) return false;
const lhs_next = lhs.next orelse return rhs.next == null;
const rhs_next = rhs.next orelse return false;
return lhs_next.eql(rhs_next);
}
pub fn andRange(self: *List, allocator: Allocator, range: Range) !void {
if (!self.head.range.hasLeft() and !self.head.range.hasRight()) {
self.head.range = range;
return;
}
var tail = try allocator.create(Query);
tail.* = Query{
.range = range,
};
tail.range = range;
var last_tail = self.tail orelse &self.head;
last_tail.next = tail;
self.tail = tail;
}
};
pub const Group = struct {
head: List = List{},
tail: ?*List = null,
allocator: Allocator,
input: string = "",
flags: FlagsBitSet = FlagsBitSet.initEmpty(),
pub const Flags = struct {
pub const pre = 1;
pub const build = 0;
};
const Formatter = struct {
group: *const Group,
buf: string,
pub fn format(formatter: @This(), writer: *std.Io.Writer) !void {
const this = formatter.group;
if (this.tail == null and this.head.tail == null and !this.head.head.range.hasLeft()) {
return;
}
if (this.tail == null and this.head.tail == null) {
try writer.print("{f}", .{this.head.fmt(formatter.buf)});
return;
}
var list = &this.head;
while (list.next) |next| {
try writer.print("{f} && ", .{list.fmt(formatter.buf)});
list = next;
}
try writer.print("{f}", .{list.fmt(formatter.buf)});
}
};
pub fn fmt(this: *const Group, buf: string) @This().Formatter {
return .{
.group = this,
.buf = buf,
};
}
pub fn jsonStringify(this: *const Group, writer: anytype) !void {
const temp = try std.fmt.allocPrint(bun.default_allocator, "{f}", .{this.fmt()});
defer bun.default_allocator.free(temp);
try std.json.encodeJsonString(temp, .{}, writer);
}
pub fn deinit(this: *const Group) void {
var list = this.head;
var allocator = this.allocator;
while (list.next) |next| {
var query = list.head;
while (query.next) |next_query| {
query = next_query.*;
allocator.destroy(next_query);
}
list = next.*;
allocator.destroy(next);
}
}
pub fn getExactVersion(this: *const Group) ?Version {
const range = this.head.head.range;
if (this.head.next == null and
this.head.head.next == null and
range.hasLeft() and
range.left.op == .eql and
!range.hasRight())
{
if (comptime Environment.allow_assert) {
assert(this.tail == null);
}
return range.left.version;
}
return null;
}
pub fn from(version: Version) Group {
return .{
.allocator = bun.default_allocator,
.head = .{
.head = .{
.range = .{
.left = .{
.op = .eql,
.version = version,
},
},
},
},
};
}
pub const FlagsBitSet = bun.bit_set.IntegerBitSet(3);
pub fn isExact(this: *const Group) bool {
return this.head.next == null and this.head.head.next == null and !this.head.head.range.hasRight() and this.head.head.range.left.op == .eql;
}
pub fn @"is *"(this: *const Group) bool {
const left = this.head.head.range.left;
return this.head.head.range.right.op == .unset and
left.op == .gte and
this.head.next == null and
this.head.head.next == null and
left.version.isZero() and
!this.flags.isSet(Flags.build);
}
pub inline fn eql(lhs: Group, rhs: Group) bool {
return lhs.head.eql(&rhs.head);
}
pub fn toVersion(this: Group) Version {
assert(this.isExact() or this.head.head.range.left.op == .unset);
return this.head.head.range.left.version;
}
pub fn orVersion(self: *Group, version: Version) !void {
if (self.tail == null and !self.head.head.range.hasLeft()) {
self.head.head.range.left.version = version;
self.head.head.range.left.op = .eql;
return;
}
var new_tail = try self.allocator.create(List);
new_tail.* = List{};
new_tail.head.range.left.version = version;
new_tail.head.range.left.op = .eql;
var prev_tail = self.tail orelse &self.head;
prev_tail.next = new_tail;
self.tail = new_tail;
}
pub fn andRange(self: *Group, range: Range) !void {
var tail = self.tail orelse &self.head;
try tail.andRange(self.allocator, range);
}
pub fn orRange(self: *Group, range: Range) !void {
if (self.tail == null and self.head.tail == null and !self.head.head.range.hasLeft()) {
self.head.head.range = range;
return;
}
var new_tail = try self.allocator.create(List);
new_tail.* = List{};
new_tail.head.range = range;
var prev_tail = self.tail orelse &self.head;
prev_tail.next = new_tail;
self.tail = new_tail;
}
pub inline fn satisfies(
group: *const Group,
version: Version,
group_buf: string,
version_buf: string,
) bool {
return if (version.tag.hasPre())
group.head.satisfiesPre(version, group_buf, version_buf)
else
group.head.satisfies(version, group_buf, version_buf);
}
};
pub fn eql(lhs: *const Query, rhs: *const Query) bool {
if (!lhs.range.eql(rhs.range)) return false;
const lhs_next = lhs.next orelse return rhs.next == null;
const rhs_next = rhs.next orelse return false;
return lhs_next.eql(rhs_next);
}
pub fn satisfies(query: *const Query, version: Version, query_buf: string, version_buf: string) bool {
return query.range.satisfies(
version,
query_buf,
version_buf,
) and (query.next orelse return true).satisfies(
version,
query_buf,
version_buf,
);
}
pub fn satisfiesPre(query: *const Query, version: Version, query_buf: string, version_buf: string, pre_matched: *bool) bool {
if (comptime Environment.allow_assert) {
assert(version.tag.hasPre());
}
return query.range.satisfiesPre(
version,
query_buf,
version_buf,
pre_matched,
) and (query.next orelse return true).satisfiesPre(
version,
query_buf,
version_buf,
pre_matched,
);
}
pub const Token = struct {
tag: Tag = Tag.none,
wildcard: Wildcard = Wildcard.none,
pub fn toRange(this: Token, version: Version.Partial) Range {
switch (this.tag) {
// Allows changes that do not modify the left-most non-zero element in the [major, minor, patch] tuple
.caret => {
// https://github.com/npm/node-semver/blob/3a8a4309ae986c1967b3073ba88c9e69433d44cb/classes/range.js#L302-L353
var range = Range{};
if (version.major) |major| done: {
range.left = .{
.op = .gte,
.version = .{
.major = major,
},
};
range.right = .{
.op = .lt,
};
if (version.minor) |minor| {
range.left.version.minor = minor;
if (version.patch) |patch| {
range.left.version.patch = patch;
range.left.version.tag = version.tag;
if (major == 0) {
if (minor == 0) {
range.right.version.patch = patch +| 1;
} else {
range.right.version.minor = minor +| 1;
}
break :done;
}
} else if (major == 0) {
range.right.version.minor = minor +| 1;
break :done;
}
}
range.right.version.major = major +| 1;
}
return range;
},
.tilda => {
// https://github.com/npm/node-semver/blob/3a8a4309ae986c1967b3073ba88c9e69433d44cb/classes/range.js#L261-L287
var range = Range{};
if (version.major) |major| done: {
range.left = .{
.op = .gte,
.version = .{
.major = major,
},
};
range.right = .{
.op = .lt,
};
if (version.minor) |minor| {
range.left.version.minor = minor;
if (version.patch) |patch| {
range.left.version.patch = patch;
range.left.version.tag = version.tag;
}
range.right.version.major = major;
range.right.version.minor = minor +| 1;
break :done;
}
range.right.version.major = major +| 1;
}
return range;
},
.none => unreachable,
.version => {
if (this.wildcard != Wildcard.none) {
return Range.initWildcard(version.min(), this.wildcard);
}
return .{ .left = .{ .op = .eql, .version = version.min() } };
},
else => {},
}
return switch (this.wildcard) {
.major => .{
.left = .{ .op = .gte, .version = version.min() },
.right = .{
.op = .lte,
.version = .{
.major = std.math.maxInt(u64),
.minor = std.math.maxInt(u64),
.patch = std.math.maxInt(u64),
},
},
},
.minor => switch (this.tag) {
.lte => .{
.left = .{
.op = .lte,
.version = .{
.major = version.major orelse 0,
.minor = std.math.maxInt(u64),
.patch = std.math.maxInt(u64),
},
},
},
.lt => .{
.left = .{
.op = .lt,
.version = .{
.major = version.major orelse 0,
.minor = 0,
.patch = 0,
},
},
},
.gt => .{
.left = .{
.op = .gt,
.version = .{
.major = version.major orelse 0,
.minor = std.math.maxInt(u64),
.patch = std.math.maxInt(u64),
},
},
},
.gte => .{
.left = .{
.op = .gte,
.version = .{
.major = version.major orelse 0,
.minor = 0,
.patch = 0,
},
},
},
else => unreachable,
},
.patch => switch (this.tag) {
.lte => .{
.left = .{
.op = .lte,
.version = .{
.major = version.major orelse 0,
.minor = version.minor orelse 0,
.patch = std.math.maxInt(u64),
},
},
},
.lt => .{
.left = .{
.op = .lt,
.version = .{
.major = version.major orelse 0,
.minor = version.minor orelse 0,
.patch = 0,
},
},
},
.gt => .{
.left = .{
.op = .gt,
.version = .{
.major = version.major orelse 0,
.minor = version.minor orelse 0,
.patch = std.math.maxInt(u64),
},
},
},
.gte => .{
.left = .{
.op = .gte,
.version = .{
.major = version.major orelse 0,
.minor = version.minor orelse 0,
.patch = 0,
},
},
},
else => unreachable,
},
.none => .{
.left = .{
.op = switch (this.tag) {
.gt => .gt,
.gte => .gte,
.lt => .lt,
.lte => .lte,
else => unreachable,
},
.version = version.min(),
},
},
};
}
pub const Tag = enum {
none,
gt,
gte,
lt,
lte,
version,
tilda,
caret,
};
pub const Wildcard = enum {
none,
major,
minor,
patch,
};
};
pub fn parse(
allocator: Allocator,
input: string,
sliced: SlicedString,
) bun.OOM!Group {
var i: usize = 0;
var list = Group{
.allocator = allocator,
.input = input,
};
var token = Token{};
var prev_token = Token{};
var count: u8 = 0;
var skip_round = false;
var is_or = false;
while (i < input.len) {
skip_round = false;
switch (input[i]) {
'>' => {
if (input.len > i + 1 and input[i + 1] == '=') {
token.tag = .gte;
i += 1;
} else {
token.tag = .gt;
}
i += 1;
while (i < input.len and input[i] == ' ') : (i += 1) {}
},
'<' => {
if (input.len > i + 1 and input[i + 1] == '=') {
token.tag = .lte;
i += 1;
} else {
token.tag = .lt;
}
i += 1;
while (i < input.len and input[i] == ' ') : (i += 1) {}
},
'=', 'v' => {
token.tag = .version;
is_or = true;
i += 1;
while (i < input.len and input[i] == ' ') : (i += 1) {}
},
'~' => {
token.tag = .tilda;
i += 1;
if (i < input.len and input[i] == '>') i += 1;
while (i < input.len and input[i] == ' ') : (i += 1) {}
},
'^' => {
token.tag = .caret;
i += 1;
while (i < input.len and input[i] == ' ') : (i += 1) {}
},
'0'...'9', 'X', 'x', '*' => {
token.tag = .version;
is_or = true;
},
'|' => {
i += 1;
while (i < input.len and input[i] == '|') : (i += 1) {}
while (i < input.len and input[i] == ' ') : (i += 1) {}
is_or = true;
token.tag = Token.Tag.none;
skip_round = true;
},
'-' => {
i += 1;
while (i < input.len and input[i] == ' ') : (i += 1) {}
},
' ' => {
i += 1;
while (i < input.len and input[i] == ' ') : (i += 1) {}
skip_round = true;
},
else => {
i += 1;
token.tag = Token.Tag.none;
// skip tagged versions
// we are assuming this is the beginning of a tagged version like "boop"
// "1.0.0 || boop"
while (i < input.len and input[i] != ' ' and input[i] != '|') : (i += 1) {}
skip_round = true;
},
}
if (!skip_round) {
const parse_result = Version.parse(sliced.sub(input[i..]));
const version = parse_result.version.min();
if (version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true);
if (version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true);
token.wildcard = parse_result.wildcard;
i += parse_result.len;
const rollback = i;
const maybe_hyphenate = i < input.len and (input[i] == ' ' or input[i] == '-');
// TODO: can we do this without rolling back?
const hyphenate: bool = maybe_hyphenate and possibly_hyphenate: {
i += strings.lengthOfLeadingWhitespaceASCII(input[i..]);
if (!(i < input.len and input[i] == '-')) break :possibly_hyphenate false;
i += 1;
i += strings.lengthOfLeadingWhitespaceASCII(input[i..]);
if (i == input.len) break :possibly_hyphenate false;
if (input[i] == 'v' or input[i] == '=') {
i += 1;
}
if (i == input.len) break :possibly_hyphenate false;
i += strings.lengthOfLeadingWhitespaceASCII(input[i..]);
if (i == input.len) break :possibly_hyphenate false;
if (!(i < input.len and switch (input[i]) {
'0'...'9', 'X', 'x', '*' => true,
else => false,
})) break :possibly_hyphenate false;
break :possibly_hyphenate true;
};
if (!hyphenate) i = rollback;
i += @as(usize, @intFromBool(!hyphenate));
if (hyphenate) {
const second_parsed = Version.parse(sliced.sub(input[i..]));
var second_version = second_parsed.version.min();
if (second_version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true);
if (second_version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true);
const range: Range = brk: {
switch (second_parsed.wildcard) {
.major => {
// "1.0.0 - x" --> ">=1.0.0"
break :brk Range{
.left = .{ .op = .gte, .version = version },
};
},
.minor => {
// "1.0.0 - 1.x" --> ">=1.0.0 < 2.0.0"
second_version.major +|= 1;
second_version.minor = 0;
second_version.patch = 0;
break :brk Range{
.left = .{ .op = .gte, .version = version },
.right = .{ .op = .lt, .version = second_version },
};
},
.patch => {
// "1.0.0 - 1.0.x" --> ">=1.0.0 <1.1.0"
second_version.minor +|= 1;
second_version.patch = 0;
break :brk Range{
.left = .{ .op = .gte, .version = version },
.right = .{ .op = .lt, .version = second_version },
};
},
.none => {
break :brk Range{
.left = .{ .op = .gte, .version = version },
.right = .{ .op = .lte, .version = second_version },
};
},
}
};
if (is_or) {
try list.orRange(range);
} else {
try list.andRange(range);
}
i += second_parsed.len + 1;
} else if (count == 0 and token.tag == .version) {
switch (parse_result.wildcard) {
.none => {
try list.orVersion(version);
},
else => {
try list.orRange(token.toRange(parse_result.version));
},
}
} else if (count == 0) {
// From a semver perspective, treat "--foo" the same as "-foo"
// example: foo/bar@1.2.3@--canary.24
// ^
if (token.tag == .none) {
is_or = false;
token.wildcard = .none;
prev_token.tag = .none;
continue;
}
try list.andRange(token.toRange(parse_result.version));
} else if (is_or) {
try list.orRange(token.toRange(parse_result.version));
} else {
try list.andRange(token.toRange(parse_result.version));
}
is_or = false;
count += 1;
token.wildcard = .none;
prev_token.tag = token.tag;
}
}
return list;
}
const string = []const u8;
const std = @import("std");
const Allocator = std.mem.Allocator;
const bun = @import("bun");
const Environment = bun.Environment;
const OOM = bun.OOM;
const assert = bun.assert;
const default_allocator = bun.default_allocator;
const strings = bun.strings;
const Range = bun.Semver.Range;
const SlicedString = bun.Semver.SlicedString;
const Version = bun.Semver.Version;