ESC
其他 17 分钟阅读

Zig v0.17.0

Zig v0.17.0

来源:Hacker News

@divCeil(5, 3) == 2 @divCeil(-5, 3) == -1

As with the other division builtins, caller guarantees that denominator != 0 and that result does not overflow.

No more std.math.divCeil(a, b) catch unreachable!

Previously, @hasDecl returned true for public declarations and declarations in the same file. Now, the behavior is the same independently of which file @hasDecl is in.

`const std = @import(“std”);

const Foo = struct { bar: i32,

const baz = 1; pub var quux = “xxx”; };

test “@hasDecl example” { try std.testing.expect(!@hasDecl(Foo, “bar”)); try std.testing.expect(!@hasDecl(Foo, “baz”)); // different in 0.17.0 try std.testing.expect(@hasDecl(Foo, “quux”)); }`

Shell

$ zig test hasdecl.zig
1/1 hasdecl.test.@hasDecl example...OK
All 1 tests passed.

Allow Dereference and Coercion to Array Pointer of comptime Length Slices §

Now allowed:

const slice: []const u16 = &.{ 1, 2, 3 }; const array: [3]u16 = slice.*; const array_ptr: *const [3]u16 = slice;

void{} Syntax Removed §

void{} is no longer valid syntax. Use {} instead (#15213).

errdefer |err| { // errwas the error being returned; you could do stuff with it! // Like a normaldefer/errdefer, you can’t return or try in this block, so you can’t change // which error is returned; but you can e.g. log it }`

The capture (|err|) is no longer allowed (#23734).

`fn processOneTarget(job: Job) void {

  • errdefer |err| std.debug.panic(“panic: {s}”, .{@errorName(err)});
  • processOneTargetInner(job) catch |err| std.debug.panic(“panic: {s}”, .{@errorName(err)}); +} +fn processOneTargetInner(job: Job) !void { const target = job.target; `

i0 Removed §

i0 is no longer an allowed primitive integer type.

This type was nonsensical, so does not have a direct alternative. However, any uses of it can almost certainly be transparently replaced with u0.

The internal and link_once tags of std.lang.GlobalLinkage have been removed as they had unclear semantics and incomplete support in codegen and linking (#36956). Any use of link_once is likely served by weak, while the replacement for internal is to simply not @export the symbol in the first place.

StackFallbackAllocator is an abstraction that is useful for the “small vec” optimization, in which common cases can fit on a pre-allocated stack buffer, but rare cases need dynamic heap allocation. The previous design had a few problems:

Now, the buffer is provided as an argument, like most other std APIs that need a buffer.

var stack align(@max( @alignOf(std.heap.StackFallbackAllocator(0)), @alignOf(Item)), ) = std.heap.stackFallback(@sizeOf(Item), self.gpa); const allocator = stack.get();

⬇️

var stack_buf: [1]Item = undefined; var stack: std.heap.StackFallbackAllocator = .init(@ptrCast(&stack_buf), gpa); const allocator = stack.allocator();

SafeAllocator Introduced §

std.heap.DebugAllocator is replaced by a thread-safe allocator with the following guarantees:

Given the backing allocator does not reuse memory, it does not reuse memory either and most writes after free will segmentation fault or are eventually detected and panic.

std.heap.DebugAllocator and std.heap.Check are deprecated.

Every allocation is trailed by an AllocFooter which contains metadata for the allocation and stack traces. It is protected by a checksum to catch corruption from allocation overwrites and report canary mismatches. An allocation’s memory has a minimum alignment of AllocFooter so that the footer is at a fixed offset determined from the allocation size. An allocation’s memory is stored either:

To track allocations, each thread maintains a table of backing allocations. The table may be modified by other threads in the case of a producer-consumer operation, so the table is a linked list only expanded by creating new segments. Each thread maintains a linked list of free entries, which may contain entries from other threads’ tables.

In the case of producer-consumer operations, acquire/release ordering is assumed to be provided externally. This is also assumed by all other thread-safe allocators that reuse memory as otherwise there would be data races on reuse of allocated memory.

Two fuzz tests have also been added for the allocator. They check that there is no memory reuse, that returned memory is writable, and that it is not overwritten. The multi-threaded fuzz test spawns a number of worker threads which are used for all the test runs. I have run these tests extensively under TSAN.

Building the standard library tests with an -Osafe compiler build and -Ddebug-allocator:

Benchmark 1 (3 runs): ./master-out/bin/zig test --zig-lib-dir lib lib/std/std.zig -femit-bin=test --test-no-exec measurement mean ± σ min … max outliers delta wall_time 29.4s ± 157ms 29.2s … 29.5s 0 ( 0%) 0% peak_rss 2.24GB ± 3.49MB 2.23GB … 2.24GB 0 ( 0%) 0% cpu_cycles 143G ± 999M 142G … 144G 0 ( 0%) 0% instructions 268G ± 5.22M 268G … 268G 0 ( 0%) 0% cache_references 13.1G ± 88.8M 13.0G … 13.2G 0 ( 0%) 0% cache_misses 2.38G ± 30.7M 2.35G … 2.41G 0 ( 0%) 0% branch_misses 634M ± 6.22M 629M … 641M 0 ( 0%) 0% Benchmark 2 (3 runs): ./branch-out/bin/zig test --zig-lib-dir lib lib/std/std.zig -femit-bin=test --test-no-exec measurement mean ± σ min … max outliers delta wall_time 22.1s ± 88.6ms 22.0s … 22.2s 0 ( 0%) ⚡- 24.7% ± 1.0% peak_rss 1.11GB ± 799KB 1.11GB … 1.11GB 0 ( 0%) ⚡- 50.3% ± 0.3% cpu_cycles 136G ± 480M 136G … 137G 0 ( 0%) ⚡- 4.4% ± 1.2% instructions 273G ± 2.07M 273G … 273G 0 ( 0%) 💩+ 1.6% ± 0.0% cache_references 12.3G ± 71.3M 12.2G … 12.4G 0 ( 0%) ⚡- 6.0% ± 1.4% cache_misses 2.02G ± 11.5M 2.01G … 2.03G 0 ( 0%) ⚡- 14.9% ± 2.2% branch_misses 569M ± 2.65M 567M … 572M 0 ( 0%) ⚡- 10.2% ± 1.7%

ArrayList §

  • getLastOrNull has been deprecated and renamed to last
  • getLast has been deprecated in favor of last combined with .?
  • lastPtr has been added which returns ?*T

Upgrade guide:

if (list.getLastOrNull()) |foo| { // ... } const foo = list.getLast();

⬇️

if (list.last()) |foo| { // ... } const foo = list.last().?;

ArrayList Pointer Stability §

This is an enhancement that can help track down ArrayList usage bugs faster (#36239).

The existing lock and unlock methods continue to act exclusively. They should be used when data may be mutated. New methods, lockShared and unlockShared, may be used for shared locking in situations where multiple independent users are reading but not mutating data.

try std.fmt.allocPrint(arena, "{s}={d}", .{ x, y });

⬇️

try arena.print("{s}={d}", .{ x, y });

Formatted Printing Enhancements §

The "{q}" specifier which escapes strings so that they can appear in double-quoted string literals has relaxed escaping rules such that UTF-8 encoded data can pass through unmangled.

"{qf}" is introduced for double-quote escaping the output of a format().

std.zon.parse now takes struct args and allocates its result from an arena.

var diag: Diagnostics = .{}; defer diag.deinit(gpa); const result = std.zon.fromSlice( MyZonType, gpa, source, &diag, .{}, ) catch |err| switch (err) { error.ParseZon => std.process.fatal("input.zon: {f}", .{diag}), error.OutOfMemory => |e| return e, }; defer std.zon.parse.free(result);

⬇️

var diag: Diagnostics = undefined; const result = std.zon.fromSlice(MyZonType, .{ .gpa = gpa, .arena = arena, .source = source, .diagnostics = &diag, }) catch |err| switch (err) { error.ParseZon => diag.fatal("input.zon"), error.OutOfMemory => |e| return e, }

Some methods were renamed:

“updateFrom” variants such as updateFromSlice were added. These update an in memory value, overwriting the value’s fields with fields specified in the ZON source. This can be useful when using ZON to load configuration files with varying precedence, for example a text editor that has a global config file and a per-project config file.

Renames the types for consistency, deprecating the previous names and the managed variant.

When doing type reflection, structs and unions return their information in struct-of-arrays style (#35234).

`— a/lib/compiler/Maker/ScannedConfig.zig +++ b/lib/compiler/Maker/ScannedConfig.zig @@ -49,9 +49,10 @@ pub fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void { }

fn printStruct(sc: *const ScannedConfig, s: *Serializer.Struct, comptime S: type, v: S) !void {

  • inline for (@typeInfo(S).@“struct”.fields) |field| {
  • try s.fieldPrefix(field.name);
  • try printValue(sc, s.container.serializer, field.type, @field(v, field.name));
  • const info = @typeInfo(S).@“struct”;
  • inline for (info.field_names, info.field_types) |field_name, field_type| {
  • try s.fieldPrefix(field_name);
  • try printValue(sc, s.container.serializer, field_type, @field(v, field_name)); } } `

Rename lang.OptimizeMode to lang.Optimize §

And remove “release” from the enum tag names.

No functional change, however, despite the addition of backwards-compatibile declarations in this patch, it is breaking because expressions that use == or != operators will not able to use the deprecated names.

std.lang.Optimize.runtimeSafety is preferred as an alternative to std.debug.runtime_safety since it will offer callsites knowledge about their own module rather than standard library module.

The redundant constants cpu, os, abi, and object_format in @import("builtin") have been deprecated and will be removed in 0.18.0. Please replace any usage with the corresponding fields on the target constant:

The functions std.mem.eql and std.mem.findDiff short-circuit when their two inputs are slices to the same memory. This short-circuiting is only correct when the == operator, for the given type, is reflexive. This isn’t the case for floats, as for example std.math.nan(f64) != std.math.nan(f64). The change in this PR disables that optimisation when working on float slices.

Previously-failing, now-succeeding tests:

const x: [3]f64 = .{ 42.0, std.math.nan(f64), 3.1415 }; try std.testing.expect(!std.mem.eql(f64, &x, &x)); try std.testing.expectEqual(1, std.mem.findDiff(f64, &x, &x));

Decouple Uri and net.HostName §

Uri was sometimes using HostName.validate for host (in resolveInPlace) and sometimes not (in parseAfterScheme). On its own, this was a problem, but the bigger problem is that RFC3986 (Uri) has a much different idea of what a valid host name is than RFC1123 (HostName), and so just making Uri consistently use HostName.validate would make Uri less useful overall.

Instead, all HostName-related stuff has been removed from Uri. Uri.getHost has been moved to HostName.fromUri (without a graceful deprecation, since the semantics are different enough for users to need to evaluate usage sites), while Uri.getHostAlloc has been removed entirely.

var host_buf: [HostName.max_len]u8 = undefined; const host = try uri.getHost(&host_buf);

⬇️

var host_buf: [HostName.max_len]u8 = undefined; // note: the error set is different than it was before since fromUri does validation const host = try HostName.fromUri(uri, &host_buf);

#36036

zig build now runs projects’ build.zig code in a separate executable than the one that performs Package Management and executes the build graph, making zig build faster for several reasons (#35428):

Furthermore, configuration is now serialized into a compact binary format that can be consumed by third party tooling and is part of the new Build Server Protocol. The prior way of satisfying this use case by forking the build runner is no longer supported.

To render configuration as .zon to stdout, pass --print-configuration.

All four combinations are possible (is_directory=true/false, metadata_mode=true/false). These features are exposed as new API in the Build System.

Since the Zig toolchain is heavily reliant on the caching system, this release also switches to a binary format, saving roughly 25% on file size, which eases a bit of pressure on the file system cache while also simplifying the work the computer needs to do - directly copy bytes from disk rather than parsing text files. The new zig cache-cat subcommand is available for troubleshooting or tinkering with files inside a zig-cache directory.

The cache system also now has the capability to explain why a “miss” happened. The public-facing API of std.Build.Cache has many breaking changes, but outside of compiler tooling, this is an uncommon API to be used, and all the changes make it harder to misuse.

This change has been observed to speed up cache hits by 5-10% (#36822).

If the cache is poisoned means that the configure logic had side effects, or otherwise did something that could not be tracked by the cache system.

This is not to be confused with whether individual steps may have side effects when being evaluated; it has to do with the logic inside build.zig itself. For example, a Run step that prints “hello world” has side effects at make time and therefore does not warrant setting this flag, while checking for the existence of scdoc at configure time in order to choose the default value for a configuration option does.

Keeping the cache pure will make zig build faster, bypassing the configurer process when identical configuration would be generated.

When the cache is poisoned, the maker process will delete the build configuration file upon ingesting it since it cannot be reused.

Ways to poison the cache include calling findProgram, or more directly std.Build.Graph.poisonCache. A better alternative than cache poisoning is to explicitly declare the configuration dependencies with these new functions:

Advanced users can override the cache poisoning behavior with a new CLI option:

--cache-poison[=mode] Override configuration caching behavior pure (default) Avoid false positive cache hits poisoned Don't cache the configuration disallowed Panics when cache would be poisoned ignored A little poison never hurt anybody

findProgram §

Immediately (in the configure phase), searches for an executable on the host that has more than one possible name.

Names are searched in order, observing search prefixes first and then PATH environment variable.

Calling this function poisons the configuration cache, so it is only appropriate when the existence of the program or its output needs to be observed by configuration logic. That’s why there is also findProgramLazy now.

Creates an anonymous Step that searches for an executable on the host that has more than one possible name.

Unlike findProgram, this function does not poison the configuration cache, however the result cannot be used in the configuration phase, hence the return type being LazyPath.

Returns the LazyPath of the found executable. The search only takes place if the LazyPath will be used by a depending Step.

This API is useful in the following cases:

In the Run step, passthru args are all together now, not observable in configure phase whether run args are provided.

`— build.zig +++ build.zig @ -if (b.args) |args| {

  • run_cmd.addArgs(args); -} +run_cmd.addPassthruArgs(); ` This removes a capability from build scripts since they can no longer observe those arguments. In exchange, it means that when changing those arguments, build scripts no longer must be rebuilt from source.

paths and exclude_paths are now LazyPath lists. There is a convenience method to create them: b.pathList.

`— build.zig +++ build.zig @

  • const fmt_include_paths = &.{ “lib”, “src”, “test”, “tools”, “build.zig”, “build.zig.zon” };
  • const fmt_exclude_paths = &.{ “test/cases”, “test/behavior/zon” };
  • const fmt_include_paths = b.pathList(&.{ “lib”, “src”, “test”, “tools”, “build.zig”, “build.zig.zon” });
  • const fmt_exclude_paths = b.pathList(&.{ “test/cases”, “test/behavior/zon” }); `

Step.Options: add addOptionPathDirectory §

Now, when adding an option that is a file path, one must explicitly choose between (#36876):

There is no concept of a “build runner” any more; it has been split into: configurer and maker

This use case is now handled by the Build Server Protocol.

All package management functionality has been moved out of the Compiler and into the Build System. This includes the following sub-commands:

This means that large parts of what used to be included in the compiler executable are now shipped in source form instead, including:

All of this functionality is now compiled in -Osafe optimization mode rather than -Ofast due to being in the compiler. When hacking on the build system itself, the environment variable ZIG_DEBUG_CMD=1 may be used to compile the build system in debug mode instead.

--pkg-path CLI arg and ZIG_LOCAL_PKG_DIR env var are now observed for both fetch and build commands.

Now zig fetch only fetches into the global cache, just like it used to. However, if --save (or any variant) is used, then it also fetches into the local package path. When fetching globally, does not require build.zig to be present. zig build always fetches locally (in addition to globally).

Notably, this fixes the regressed use case zig fetch .

When fetching by path, the hash is always computed, recompressed tarball is always created, always overwrites any existing global cache entry.

It used to be the case that, when targeting Windows or Wine, artifact args added to Run steps modified PATH based on the set of directories containing the recursive set of DLL dependencies. Now this is only done for argv[0]. The motivation for also doing this for the other command line arguments is unclear, since those DLLs don’t need to be loaded in order to execute argv[0].

Now, when --listen=- is passed, the build system serves a protocol that allows connected clients to monitor and control the build graph as it executes. This is intended to be consumed by third-party tooling such as IDEs.

In particular, the separation of maker process and configurer process is a breaking change that prevents the ZLS project from working with 0.17.0. Although some progress was made to restore functionality in this release cycle, Zig team and ZLS team are still working together to enhance the build server protocol further to the point that ZLS can not only restore functionality, but surpass the power and capabilities compared to before.

In the future it is expected for much of Zig’s own first-party build system tooling to become a client of the build server protocol, dogfooding it to ensure that third-party tooling enjoys equivalent capabilities (#36497).

It is also planned for the build server to multiplex compiler server protocol for the compilation steps, providing type-system information, refactoring, and other advanced editing capabilities (#615).

The Zig compiler’s implementation of incremental compilation—a feature allowing near-instant rebuilds of projects after changing the code—has been significantly improved in Zig 0.17.0. Many bugs have been fixed, and the new ELF Linker introduced in the previous release has gained good support for the feature.

Thanks to these enhancements, it is now possible for most projects targeting x86_64-linux to take advantage of incremental compilation. To do so, add the arguments -fincremental --watch to your zig build command (e.g. zig build -fincremental --watch)—this will cause the Zig build system to listen for changes to source files, and react to them by performing an incremental rebuild.

For more information on ways to use incremental compilation in your own projects, or to learn more about how this feature works under the hood, consider checking out this blog post by a Zig core team member.

Future releases will continue to focus on improving this feature, including introducing a new Mach-O linker and self-hosted aarch64 Backend with good support for incremental compilation; adding support for using incremental compilation without --watch; and fixing any remaining bugs.

The self-hosted SPIR-V backend is now multi-threaded like the other backends.

Execution modes such as LocalSize and OriginUpperLeft are now derived from the function’s calling convention instead of being set through inline assembly, and the new spirv_task and spirv_mesh calling conventions add support for task and mesh shaders (#35676).

Declaring capabilities and extensions in inline assembly with OpCapability and OpExtension is no longer allowed. They are enabled through target CPU features instead, i.e. the -mcpu option.

22 bugs were fixed in the SPIR-V backend during this release cycle.

Progress towards this is blocked on Linker enhancements, many of which were completed during this release cycle.

Initial implementation of self-hosted backend for loongarch64 has been contributed (#36418). It is still experimental and not yet usable. There are two ways to contribute to this backend: working on it directly, and contributing to AIR Legalization Features, which helps all unfinished backends reach the finish line quicker.

Zig’s WebAssembly backend is now passing 2060/2054 (100%) behavior tests compared to the LLVM backend. However, it is not yet the default when compiling in debug optimization mode due to lack of debug info support (#37032).

This release makes significant progress towards replacing Zig’s legacy self-hosted ELF linker with its new implementation introduced in the previous release. Specific enhancements include:

While this linker has not quite reached feature parity with our old self-hosted ELF linker yet—and so remains disabled by default—it is already capable in practice of building the vast majority of Zig projects targeting x86_64-linux. This unlocks the ability to use Incremental Compilation for these projects—like in Zig 0.16.0, the new linker is enabled by default in this case.

In the next release of Zig, we hope to fully eliminate the legacy ELF linker in favour of this implementation.

COFF support in the linker is enhanced with the following features (#35674):

Zig is moving towards snapshot-based testing for its linkers.

Tests are a combination of comparing objdump snapshot output, actually running the artifacts, and checking for linker errors.

zig build -Dlink-snapshot-update causes tests to run in a mode that outputs snapshots instead of checking against them.

A typical workflow for adding a new test:

The SPIR-V linker has been rewritten (#36828). It now supports incremental compilation and can link external .spv object files.

Although this release’s Build System changes are loosely related to Zig’s integrated fuzzer and its interaction with the build system, no changes were made to the fuzzer itself.

We expect to focus on improving the fuzzer in a future release cycle.

Full list of the 329 bug reports closed during this release cycle:

Many bugs were both introduced and resolved within this release cycle. Most bug fixes are omitted from these release notes for the sake of brevity.