I think the simplest way would be to have a concrete comptime type like Unavailable below, defined as part of the standard library.
Unavailable can have a message and it uses ComptimeAny to hold an arbitrary comptime value for further information (based on what makes sense to represent the reason), you can print that value or write comptime code that reflects on its fields.
ComptimeAny works in a similar way like the default value of a StructField, pairing a type and a *const anyopaque.
With that we have a type that works at comptime, however we need unavailable.ctx() which creates an instance of Unavailable.Msg(<msg>) to get the compile error when somebody tries to use the value at runtime where something else was expected (to have the message appear in the compile error), for that we comptimePrint the Unavailable-instance to a string literal.
But now our Unavailable.check function needs to return true for those Msg instances (so that we can check for unavailable things at comptime), for that we check whether it looks like a Msg and if so, we access and reconstruct Msg and then compare against the reconstructed generic type.
So here is the basic sketch, using the scenarios described by @chung-leong:
pub fn main() !void {
// _ = &foo(); // with -fsingle-threaded results in:
// unavailable.zig:2:10: error: type 'unavailable.Unavailable.Msg("Unavailable: Not available in single-threaded environment\n\rValue:\n\r .{ .reason = .needs_concurrency }\n\r"[0..106])' not a function
// _ = &foo();
// ^~~
if (comptime Unavailable.check(&foo)) {
std.debug.print("functionality is not available\n", .{});
std.debug.print("{f}\n", .{&foo});
} else {
std.debug.print("{*}\n", .{&foo});
}
example(.valMsg(TODO, "Add support for color escape sequences"));
example(.val(Removed{ .issue = 1234 }));
example(.val(Moved{ .to = "std.new_location.Here" }));
example(.val(Deprecated(std.array_list.Managed, std.ArrayListUnmanaged)));
example(.val(untranslatable("some_header.h", 25, 15)));
example(.valMsg(Uncertain, "could take between 1 and 122 business days"));
}
pub fn example(comptime unavailable: Unavailable) void {
std.debug.print("{f}\n", .{unavailable.ctx()});
}
pub const foo = switch (builtin.single_threaded) {
// true => Unavailable.msg("Not available in single-threaded environment"),
true => Unavailable.msgVal(
"Not available in single-threaded environment",
NotAvailableInSingleThreaded{ .reason = .needs_concurrency },
).ctx(),
false => fooAsync,
};
pub const TODO = Unavailable.Prefix("TO" ++ "DO "){};
pub const Removed = struct {
pub const mode: Unavailable.Mode = .prefix_value;
pub const prefix = "Removed ";
issue: u32,
};
pub const Moved = struct {
pub const mode: Unavailable.Mode = .prefix_value;
pub const prefix = "Moved ";
to: []const u8,
pub fn format(
self: Moved,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
try writer.print("to: {[to]s}", self);
}
};
pub fn Deprecated(comptime old: anytype, comptime new: anytype) type {
return struct {
pub const @"FIXME!" = old;
pub const @"NEW!" = new;
};
}
pub const Untranslatable = struct {
pub const mode: Unavailable.Mode = .prefix_value;
pub const prefix = "Untranslatable ";
pub const Location = struct {
file: [:0]const u8,
line: u32,
column: u32,
};
loc: Location,
pub fn format(
self: Untranslatable,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
try writer.print("{[file]s}:{[line]}:{[column]}", self.loc);
}
};
pub fn untranslatable(file: [:0]const u8, line: u32, column: u32) Untranslatable {
return .{ .loc = .{ .file = file, .line = line, .column = column } };
}
pub const Uncertain = Unavailable.Prefix("Uncertain: "){};
pub const NotAvailableInSingleThreaded = struct {
pub const Reason = enum { needs_concurrency, not_implemented };
reason: Reason,
};
pub fn fooAsync() !void {}
//////////////////////////////
// impl below
pub const Unavailable = struct {
pub fn Msg(comptime reason_: []const u8) type {
return struct {
pub const reason = reason_;
unavailable: Unavailable,
pub fn format(
self: @This(),
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
try writer.print("{f}", .{self.unavailable});
}
};
}
pub fn ctx(comptime self: Unavailable) Msg(self.str()) {
return .{ .unavailable = self };
}
pub const Mode = enum {
value,
prefix,
prefix_value,
pub fn showPrefix(m: Mode) bool {
return switch (m) {
.prefix, .prefix_value => true,
else => false,
};
}
pub fn showValue(m: Mode) bool {
return switch (m) {
.value, .prefix_value => true,
else => false,
};
}
};
message: []const u8,
any: ComptimeAny,
pub fn valMsg(comptime value: anytype, msg_: []const u8) Unavailable {
return .msgVal(msg_, value);
}
pub fn msgVal(msg_: []const u8, comptime value: anytype) Unavailable {
return .{ .message = msg_, .any = .init(value) };
}
pub fn msg(msg_: []const u8) Unavailable {
return .msgVal(msg_, {});
}
pub fn val(comptime value: anytype) Unavailable {
return .msgVal("", value);
}
pub fn format(
self: Unavailable,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
const mode: Mode = comptime if (hasDecl(self.any.type, "mode")) self.any.type.mode else .value;
const multiline = std.mem.count(u8, self.message, "\n") > 0;
try writer.writeAll("Unavailable: ");
if (comptime mode.showPrefix()) {
if (comptime !hasDecl(self.any.type, "prefix"))
@compileError("expected type '" ++ @typeName(self.any.type) ++ "' to have a declaration 'prefix' of type []const u8");
try writer.print("{s}", .{self.any.type.prefix});
}
if (multiline) {
var it = std.mem.splitScalar(u8, self.message, '\n');
while (it.next()) |line| {
try writer.print("\n\r {s}", .{line});
}
} else {
try writer.print("{s}", .{self.message});
}
if (comptime mode.showValue()) {
// try writer.print("{s}\n\r", .{@typeName(self.any.type)});
try writer.writeAll("\n\rValue:\n\r ");
try writer.print("{f}\n\r", .{self.any});
}
}
pub fn str(comptime self: Unavailable) *const [std.fmt.count("{f}", .{self}):0]u8 {
return std.fmt.comptimePrint("{f}", .{self});
}
pub fn Prefix(comptime prefix_: []const u8) type {
const Impl = struct {
pub const mode: Unavailable.Mode = .prefix;
pub const prefix = prefix_;
};
return Impl;
}
pub fn check(v: anytype) bool {
const T = @TypeOf(v);
return switch (@typeInfo(T)) {
.type => v == Unavailable,
.pointer => |p| if (p.child == Unavailable) true else (if (p.size == .one) check(v.*) else check(p.child)),
// zig fmt: off
.@"struct" => T == Unavailable or
(hasField(T, "unavailable") and
@FieldType(T, "unavailable") == Unavailable and
T == @TypeOf(v.unavailable.ctx())),
// zig fmt: on
else => false,
};
}
};
pub const ComptimeAny = struct {
type: type,
ptr: *const anyopaque,
pub fn init(comptime val: anytype) ComptimeAny {
return .{ .type = @TypeOf(val), .ptr = @ptrCast(&val) };
}
pub fn value(self: ComptimeAny) self.type {
const val: *const self.type = @ptrCast(@alignCast(self.ptr));
return val.*;
}
pub fn format(
self: ComptimeAny,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
if (comptime hasDecl(self.type, "format")) {
try writer.print("{f}", .{self.value()});
} else {
try writer.print("{any}", .{self.value()});
}
}
};
pub fn hasField(comptime T: type, comptime name: []const u8) bool {
return switch (@typeInfo(T)) {
.@"struct", .@"enum", .@"union" => @hasField(T, name),
else => false,
};
}
pub fn hasDecl(comptime T: type, comptime name: []const u8) bool {
return switch (@typeInfo(T)) {
.@"struct", .@"enum", .@"union", .@"opaque" => @hasDecl(T, name),
else => false,
};
}
const std = @import("std");
const builtin = @import("builtin");
And this is the output:
fn () @typeInfo(@typeInfo(@TypeOf(unavailable.fooAsync)).@"fn".return_type.?).error_union.error_set!void@113f8b0
Unavailable: TODO Add support for color escape sequences
Unavailable: Removed
Value:
.{ .issue = 1234 }
Unavailable: Moved
Value:
to: std.new_location.Here
Unavailable:
Value:
unavailable.Deprecated((function 'Managed'),(function 'ArrayList'))
Unavailable: Untranslatable
Value:
some_header.h:25:15
Unavailable: Uncertain: could take between 1 and 122 business days
When run with zig run unavailable.zig -fsingle-threaded the first line is:
functionality is not available
and the rest is the same, because those unconditionally print the different Unavailable values.
I think the specific protocol for how Unavailable prints values and what kinds of modes it supports could still be changed a lot, it is just what I came up with on the spot.