Skip to content

@deprecated() builtinΒ #22822

@kristoff-it

Description

@kristoff-it

Motivation

It's already an established pattern in the Zig ecosystem to deprecate 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:

// 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);
}
// 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):

// 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:

/// Deprecated: moved to std.Thread.sleep
pub const sleep = std.Thread.sleep;
pub const sleep = @deprecated(std.Thread.sleep);

A second example: using @deprecated in a function body:

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedThis proposal is planned.proposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions