kj4tmp
December 9, 2024, 5:35am
1
According to a quick search of the zig repo, there are 27 files with /// Deprecated
in them, most from the standard library.
Given that zig aims to enable maintainable
software, what do you think about language features regarding deprecation?
An obvious non-language-level utility would be a third-party linter looking for Deprecated
in doc comments.
What existing features of zig aid the maintainability of libraries and user code through deprecation cycles?
Some existing semi-related maintainability features I have found nice:
User code (and library code when refactoring) sometimes doesn’t need to change very much when types change from pointers to values, since fields of struct pointers can be directly accessed with .
, like my_struct_pointer.my_field
, which contrasts with C requiring the ->
(arrow) operator.
An enum type can be changed to a tagged union typically without changes to user code in switches.
3 Likes
matklad
December 9, 2024, 11:55am
2
Doesn’t need to be a language feature I think? Spitballing, the user code can do
pub const std_options = .{
.deprecation_warnings = true,
};
and then the library could do
pub fn a_deprecated_function() {
comptime if (@import("root").std_options.deperecation_warnings) {
@compileLog("'a_deprecated_function' is deprecated, use 'a_better_function' instead")
}
}
Would love for std to do this actually, it should ease Zig upgrades quite a bit!
15 Likes
alanza
December 9, 2024, 9:48pm
3
of course, status quo zig cannot do this bc std.Options
does not have a deprecation_warnings
field.
user code can do something like this today though:
const root = @import("root");
pub const aDeprecatedFunction = if (@hasDecl(root, "deprecation_warnings") and root.deprecation_warnings) {
@compileError("'aDeprecatedFunction() is deprecated, use aBetterName() instead!")
} else aBetterName;
pub fn aBetterName() { ... }
ETA – could also make “deprecation_warnings” an options module in one’s build.zig
so that users of a library could opt into them as part of the build process.
geemili
December 10, 2024, 7:54pm
4
I think for user code a build option is the way to go. If deprecation_warnings
does become a std option, then it might make sense to use an enum
instead of a bool
:
const DeprecationWarning = enum {
ignore,
compile_log,
use_std_option,
};
Then default to .use_std_option
if the user doesn’t pass in anything.
Although, I’m not sure when you would want to see only a subset of the deprecations.
kj4tmp
February 9, 2025, 7:27am
5
There is now a proposal for a @deprecated()
builtin.
opened 12:32AM - 09 Feb 25 UTC
proposal
## Motivation
It's already an established pattern in the Zig ecosystem to depre… cate declarations first via a doc comment, and after a while by turning the old decl into a `@compileError` once the deprecation "grace period" is over, to then finally delete the decl altogether after some more time.
Currently a user that is interested in discovering early which APIs are being deprecated must grep the codebase for the word, find the decl the comment refers to, and figure out if and where its being used (directly or not) in the codebase.
Conversely, a library author that wants to maintain backwards compatibility (i.e. avoid a major version bump for a v1 package) will not want to set deprecated decls to `@compileError` as that would be a compatibility breakage, never giving users an opportunity to leverage compile errors to migrate their API usage.
It is possible today to implement a better solution entirely in userland:
```zig
// build.zig
fn build(b: *std.Build) void {
const opts = b.addOptions();
opts.addOption("deprecated", b.option(
bool,
"deprecated",
"turn deprecated decls into compile errors",
) orelse false);
}
```
```zig
// root.zig
const options = @import("options");
fn oldFoo() void {
if (options.deprecated) {
@compileError("deprecated, use newFoo()");
}
}
/// Deprecated: use `GenericWriter`
const Writer = if (options.deprecated) @compileError("deprecated, use `GenericWriter`") else GenericWriter;
```
Running `zig build -Ddeprecated` will turn deprecated decls in compile errors giving both a nice compiler-driven upgrade experience to users, while letting library authors maintain backwards compatibility.
This pattern seems so useful that might be worth enshrining it as a standardized practice.
## Proposal
Introduce a `@deprecated()` builtin and a new `-fdeprecated` compiler flag to be used like so (this is a contrived example to showcase the workflow):
```zig
// main.zig
const std = @import("std");
const num = @deprecated(10);
pub fn main() void {
std.debug.print("num: {}\n", .{num});
}
```
```
$ zig run main.zig
num: 10
```
```
$ zig run main.zig -fdeprecated
main.zig:3:13: error: found deprecated code
const num = @deprecated(10);
^~~~~~~~~~~~~~~
referenced by:
main: main.zig:6:34
posixCallMainAndExit: zig/lib/std/start.zig:647:22
4 reference(s) hidden; use '-freference-trace=6' to see all references
```
A less contrived example would be the current deprecation of `std.time.sleep` in the Zig standard library:
```zig
/// Deprecated: moved to std.Thread.sleep
pub const sleep = std.Thread.sleep;
```
```zig
pub const sleep = @deprecated(std.Thread.sleep);
```
A second example: using `@deprecated` in a function body:
```zig
const Strategy = enum { greedy, expensive, fast };
fn compute(comptime strat: Strategy, comptime foo: bool, bar: usize) void {
switch(strat) {
.greedy => {
// This strategy turned out to be bad when foo is false,
// use the fast strategy instead.
if (!foo) @deprecated();
runGreedy(foo, bar);
},
.expensive => runExpensive(foo, bar),
.fast => runFast(foo, bar),
}
}
```
Lastly, since `@deprecated` conveys intent more precisely, tooling like LSPs and linters could offer diagnostics about deprecation automatically to the user in a more streamlined fashion than with the full userland solution.
A branch with `@deprecated` implemented can be found here: https://github.com/kristoff-it/zig/tree/deprecated-proposal ([diff](https://github.com/ziglang/zig/commit/da6471a209a495494074420886c70984357a8436))
2 Likes
As long as depreciation marking includes zig fmt automation when feasible!
jmc
February 9, 2025, 6:22pm
7
I wonder if instead of another builtin just to mark something deprecated it wouldn’t be better to add a builtin to attach an attribute, and the “deprecated” can just be an attribute value then.
I wonder if instead of another builtin just to mark something deprecated it wouldn’t be better to add a builtin to attach an attribute, and the “deprecated” can just be an attribute value then.
That sounds similar to various pragma/attribute issues, all of which I believe are rejected or closed.
Here’s one such issue, which has deprecation as one of the examples: Proposal: Pragmas · Issue #5239 · ziglang/zig · GitHub
kj4tmp
February 27, 2025, 6:35am
9
@deprecated()
has been merged, looking forward to trying it out! (… though not saying I look forward to writing code that I later have to remove, slowly and painfully, from my non-existent users…)
opened 12:32AM - 09 Feb 25 UTC
closed 06:31AM - 27 Feb 25 UTC
proposal
accepted
## Motivation
It's already an established pattern in the Zig ecosystem to depre… cate declarations first via a doc comment, and after a while by turning the old decl into a `@compileError` once the deprecation "grace period" is over, to then finally delete the decl altogether after some more time.
Currently a user that is interested in discovering early which APIs are being deprecated must grep the codebase for the word, find the decl the comment refers to, and figure out if and where its being used (directly or not) in the codebase.
Conversely, a library author that wants to maintain backwards compatibility (i.e. avoid a major version bump for a v1 package) will not want to set deprecated decls to `@compileError` as that would be a compatibility breakage, never giving users an opportunity to leverage compile errors to migrate their API usage.
It is possible today to implement a better solution entirely in userland:
```zig
// build.zig
fn build(b: *std.Build) void {
const opts = b.addOptions();
opts.addOption("deprecated", b.option(
bool,
"deprecated",
"turn deprecated decls into compile errors",
) orelse false);
}
```
```zig
// root.zig
const options = @import("options");
fn oldFoo() void {
if (options.deprecated) {
@compileError("deprecated, use newFoo()");
}
}
/// Deprecated: use `GenericWriter`
const Writer = if (options.deprecated) @compileError("deprecated, use `GenericWriter`") else GenericWriter;
```
Running `zig build -Ddeprecated` will turn deprecated decls in compile errors giving both a nice compiler-driven upgrade experience to users, while letting library authors maintain backwards compatibility.
This pattern seems so useful that might be worth enshrining it as a standardized practice.
## Proposal
Introduce a `@deprecated()` builtin and a new `-fdeprecated` compiler flag to be used like so (this is a contrived example to showcase the workflow):
```zig
// main.zig
const std = @import("std");
const num = @deprecated(10);
pub fn main() void {
std.debug.print("num: {}\n", .{num});
}
```
```
$ zig run main.zig
num: 10
```
```
$ zig run main.zig -fdeprecated
main.zig:3:13: error: found deprecated code
const num = @deprecated(10);
^~~~~~~~~~~~~~~
referenced by:
main: main.zig:6:34
posixCallMainAndExit: zig/lib/std/start.zig:647:22
4 reference(s) hidden; use '-freference-trace=6' to see all references
```
A less contrived example would be the current deprecation of `std.time.sleep` in the Zig standard library:
```zig
/// Deprecated: moved to std.Thread.sleep
pub const sleep = std.Thread.sleep;
```
```zig
pub const sleep = @deprecated(std.Thread.sleep);
```
A second example: using `@deprecated` in a function body:
```zig
const Strategy = enum { greedy, expensive, fast };
fn compute(comptime strat: Strategy, comptime foo: bool, bar: usize) void {
switch(strat) {
.greedy => {
// This strategy turned out to be bad when foo is false,
// use the fast strategy instead.
if (!foo) @deprecated();
runGreedy(foo, bar);
},
.expensive => runExpensive(foo, bar),
.fast => runFast(foo, bar),
}
}
```
Lastly, since `@deprecated` conveys intent more precisely, tooling like LSPs and linters could offer diagnostics about deprecation automatically to the user in a more streamlined fashion than with the full userland solution.
A branch with `@deprecated` implemented can be found here: https://github.com/kristoff-it/zig/tree/deprecated-proposal ([diff](https://github.com/ziglang/zig/commit/da6471a209a495494074420886c70984357a8436))
5 Likes