zig/test/behavior/fn.zig
mlugg 0ec6b2dd88 compiler: simplify generic functions, fix issues with inline calls
The original motivation here was to fix regressions caused by #22414.
However, while working on this, I ended up discussing a language
simplification with Andrew, which changes things a little from how they
worked before #22414.

The main user-facing change here is that any reference to a prior
function parameter, even if potentially comptime-known at the usage
site or even not analyzed, now makes a function generic. This applies
even if the parameter being referenced is not a `comptime` parameter,
since it could still be populated when performing an inline call. This
is a breaking language change.

The detection of this is done in AstGen; when evaluating a parameter
type or return type, we track whether it referenced any prior parameter,
and if so, we mark this type as being "generic" in ZIR. This will cause
Sema to not evaluate it until the time of instantiation or inline call.

A lovely consequence of this from an implementation perspective is that
it eliminates the need for most of the "generic poison" system. In
particular, `error.GenericPoison` is now completely unnecessary, because
we identify generic expressions earlier in the pipeline; this simplifies
the compiler and avoids redundant work. This also entirely eliminates
the concept of the "generic poison value". The only remnant of this
system is the "generic poison type" (`Type.generic_poison` and
`InternPool.Index.generic_poison_type`). This type is used in two
places:

* During semantic analysis, to represent an unknown result type.
* When storing generic function types, to represent a generic parameter/return type.

It's possible that these use cases should instead use `.none`, but I
leave that investigation to a future adventurer.

One last thing. Prior to #22414, inline calls were a little inefficient,
because they re-evaluated even non-generic parameter types whenever they
were called. Changing this behavior is what ultimately led to #22538.
Well, because the new logic will mark a type expression as generic if
there is any change its resolved type could differ in an inline call,
this redundant work is unnecessary! So, this is another way in which the
new design reduces redundant work and complexity.

Resolves: #22494
Resolves: #22532
Resolves: #22538
2025-01-21 02:41:42 +00:00

714 lines
19 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const assert = std.debug.assert;
const expect = testing.expect;
const expectEqual = testing.expectEqual;
test "params" {
try expect(testParamsAdd(22, 11) == 33);
}
fn testParamsAdd(a: i32, b: i32) i32 {
return a + b;
}
test "local variables" {
testLocVars(2);
}
fn testLocVars(b: i32) void {
const a: i32 = 1;
if (a + b != 3) unreachable;
}
test "mutable local variables" {
var zero: i32 = 0;
_ = &zero;
try expect(zero == 0);
var i = @as(i32, 0);
while (i != 3) {
i += 1;
}
try expect(i == 3);
}
test "separate block scopes" {
{
const no_conflict: i32 = 5;
try expect(no_conflict == 5);
}
const c = x: {
const no_conflict = @as(i32, 10);
break :x no_conflict;
};
try expect(c == 10);
}
fn @"weird function name"() i32 {
return 1234;
}
test "weird function name" {
try expect(@"weird function name"() == 1234);
}
test "assign inline fn to const variable" {
const a = inlineFn;
a();
}
inline fn inlineFn() void {}
fn outer(y: u32) *const fn (u32) u32 {
const Y = @TypeOf(y);
const st = struct {
fn get(z: u32) u32 {
return z + @sizeOf(Y);
}
};
return st.get;
}
test "return inner function which references comptime variable of outer function" {
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
const func = outer(10);
try expect(func(3) == 7);
}
test "discard the result of a function that returns a struct" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn entry() void {
_ = func();
}
fn func() Foo {
return undefined;
}
const Foo = struct {
a: u64,
b: u64,
};
};
S.entry();
comptime S.entry();
}
test "inline function call that calls optional function pointer, return pointer at callsite interacts correctly with callsite return type" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
const S = struct {
field: u32,
fn doTheTest() !void {
bar2 = actualFn;
const result = try foo();
try expect(result.field == 1234);
}
const Foo = struct { field: u32 };
fn foo() !Foo {
var res: Foo = undefined;
res.field = bar();
return res;
}
inline fn bar() u32 {
return bar2.?();
}
var bar2: ?*const fn () u32 = null;
fn actualFn() u32 {
return 1234;
}
};
try S.doTheTest();
}
test "implicit cast function unreachable return" {
wantsFnWithVoid(fnWithUnreachable);
}
fn wantsFnWithVoid(comptime f: fn () void) void {
_ = f;
}
fn fnWithUnreachable() noreturn {
unreachable;
}
test "extern struct with stdcallcc fn pointer" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_c and builtin.cpu.arch == .x86) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
const S = extern struct {
ptr: *const fn () callconv(if (builtin.target.cpu.arch == .x86) .Stdcall else .c) i32,
fn foo() callconv(if (builtin.target.cpu.arch == .x86) .Stdcall else .c) i32 {
return 1234;
}
};
var s: S = undefined;
s.ptr = S.foo;
try expect(s.ptr() == 1234);
}
const nComplexCallconv = 100;
fn fComplexCallconvRet(x: u32) callconv(blk: {
const s: struct { n: u32 } = .{ .n = nComplexCallconv };
break :blk switch (s.n) {
0 => .c,
1 => .Inline,
else => .Unspecified,
};
}) struct { x: u32 } {
return .{ .x = x * x };
}
test "function with complex callconv and return type expressions" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
try expect(fComplexCallconvRet(3).x == 9);
}
test "pass by non-copying value" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
try expect(addPointCoords(Point{ .x = 1, .y = 2 }) == 3);
}
const Point = struct {
x: i32,
y: i32,
};
fn addPointCoords(pt: Point) i32 {
return pt.x + pt.y;
}
test "pass by non-copying value through var arg" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
try expect((try addPointCoordsVar(Point{ .x = 1, .y = 2 })) == 3);
}
fn addPointCoordsVar(pt: anytype) !i32 {
comptime assert(@TypeOf(pt) == Point);
return pt.x + pt.y;
}
test "pass by non-copying value as method" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
var pt = Point2{ .x = 1, .y = 2 };
try expect(pt.addPointCoords() == 3);
}
const Point2 = struct {
x: i32,
y: i32,
fn addPointCoords(self: Point2) i32 {
return self.x + self.y;
}
};
test "pass by non-copying value as method, which is generic" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
var pt = Point3{ .x = 1, .y = 2 };
try expect(pt.addPointCoords(i32) == 3);
}
const Point3 = struct {
x: i32,
y: i32,
fn addPointCoords(self: Point3, comptime T: type) i32 {
_ = T;
return self.x + self.y;
}
};
test "pass by non-copying value as method, at comptime" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
comptime {
var pt = Point2{ .x = 1, .y = 2 };
try expect(pt.addPointCoords() == 3);
}
}
test "implicit cast fn call result to optional in field result" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn entry() !void {
const x = Foo{
.field = optionalPtr(),
};
try expect(x.field.?.* == 999);
}
const glob: i32 = 999;
fn optionalPtr() *const i32 {
return &glob;
}
const Foo = struct {
field: ?*const i32,
};
};
try S.entry();
try comptime S.entry();
}
test "void parameters" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
try voidFun(1, void{}, 2, {});
}
fn voidFun(a: i32, b: void, c: i32, d: void) !void {
_ = d;
const v = b;
const vv: void = if (a == 1) v else {};
try expect(a + c == 3);
return vv;
}
test "call function with empty string" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
acceptsString("");
}
fn acceptsString(foo: []u8) void {
_ = foo;
}
test "function pointers" {
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
const fns = [_]*const @TypeOf(fn1){
&fn1,
&fn2,
&fn3,
&fn4,
};
for (fns, 0..) |f, i| {
try expect(f() == @as(u32, @intCast(i)) + 5);
}
}
fn fn1() u32 {
return 5;
}
fn fn2() u32 {
return 6;
}
fn fn3() u32 {
return 7;
}
fn fn4() u32 {
return 8;
}
test "number literal as an argument" {
try numberLiteralArg(3);
try comptime numberLiteralArg(3);
}
fn numberLiteralArg(a: anytype) !void {
try expect(a == 3);
}
test "function call with anon list literal" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn doTheTest() !void {
try consumeVec(.{ 9, 8, 7 });
}
fn consumeVec(vec: [3]f32) !void {
try expect(vec[0] == 9);
try expect(vec[1] == 8);
try expect(vec[2] == 7);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "function call with anon list literal - 2D" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn doTheTest() !void {
try consumeVec(.{ .{ 9, 8 }, .{ 7, 6 } });
}
fn consumeVec(vec: [2][2]f32) !void {
try expect(vec[0][0] == 9);
try expect(vec[0][1] == 8);
try expect(vec[1][0] == 7);
try expect(vec[1][1] == 6);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "ability to give comptime types and non comptime types to same parameter" {
const S = struct {
fn doTheTest() !void {
var x: i32 = 1;
_ = &x;
try expect(foo(x) == 10);
try expect(foo(i32) == 20);
}
fn foo(arg: anytype) i32 {
if (@typeInfo(@TypeOf(arg)) == .type and arg == i32) return 20;
return 9 + arg;
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "function with inferred error set but returning no error" {
const S = struct {
fn foo() !void {}
};
const return_ty = @typeInfo(@TypeOf(S.foo)).@"fn".return_type.?;
try expectEqual(0, @typeInfo(@typeInfo(return_ty).error_union.error_set).error_set.?.len);
}
test "import passed byref to function in return type" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn get() @import("std").ArrayListUnmanaged(i32) {
const x: @import("std").ArrayListUnmanaged(i32) = .empty;
return x;
}
};
const list = S.get();
try expect(list.items.len == 0);
}
test "implicit cast function to function ptr" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
const S1 = struct {
export fn someFunctionThatReturnsAValue() c_int {
return 123;
}
};
var fnPtr1: *const fn () callconv(.c) c_int = S1.someFunctionThatReturnsAValue;
_ = &fnPtr1;
try expect(fnPtr1() == 123);
const S2 = struct {
extern fn someFunctionThatReturnsAValue() c_int;
};
var fnPtr2: *const fn () callconv(.c) c_int = S2.someFunctionThatReturnsAValue;
_ = &fnPtr2;
try expect(fnPtr2() == 123);
}
test "method call with optional and error union first param" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
x: i32 = 1234,
fn opt(s: ?@This()) !void {
try expect(s.?.x == 1234);
}
fn errUnion(s: anyerror!@This()) !void {
try expect((try s).x == 1234);
}
};
var s: S = .{};
try s.opt();
try s.errUnion();
}
test "method call with optional pointer first param" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
x: i32 = 1234,
fn method(s: ?*@This()) !void {
try expect(s.?.x == 1234);
}
};
var s: S = .{};
try s.method();
const s_ptr = &s;
try s_ptr.method();
}
test "using @ptrCast on function pointers" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
const S = struct {
const A = struct { data: [4]u8 };
fn at(arr: *const A, index: usize) *const u8 {
return &arr.data[index];
}
fn run() !void {
const a = A{ .data = "abcd".* };
const casted_fn = @as(*const fn (*const anyopaque, usize) *const u8, @ptrCast(&at));
const casted_impl = @as(*const anyopaque, @ptrCast(&a));
const ptr = casted_fn(casted_impl, 2);
try expect(ptr.* == 'c');
}
};
try S.run();
// https://github.com/ziglang/zig/issues/2626
// try comptime S.run();
}
test "function returns function returning type" {
const S = struct {
fn a() fn () type {
return (struct {
fn b() type {
return u32;
}
}).b;
}
};
try expect(S.a()() == u32);
}
test "peer type resolution of inferred error set with non-void payload" {
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
const S = struct {
fn openDataFile(mode: enum { read, write }) !u32 {
return switch (mode) {
.read => foo(),
.write => bar(),
};
}
fn foo() error{ a, b }!u32 {
return 1;
}
fn bar() error{ c, d }!u32 {
return 2;
}
};
try expect(try S.openDataFile(.read) == 1);
}
test "lazy values passed to anytype parameter" {
const A = struct {
a: u32,
fn foo(comptime a: anytype) !void {
try expect(a[0][0] == @sizeOf(@This()));
}
};
try A.foo(.{[_]usize{@sizeOf(A)}});
const B = struct {
fn foo(comptime a: anytype) !void {
try expect(a.x == 0);
}
};
try B.foo(.{ .x = @sizeOf(B) });
const C = struct {};
try expect(@as(u32, @truncate(@sizeOf(C))) == 0);
const D = struct {};
try expect(@sizeOf(D) << 1 == 0);
}
test "pass and return comptime-only types" {
const S = struct {
fn returnNull(comptime x: @Type(.null)) @Type(.null) {
return x;
}
fn returnUndefined(comptime x: @Type(.undefined)) @Type(.undefined) {
return x;
}
};
try expectEqual(null, S.returnNull(null));
try expectEqual(@as(u0, 0), S.returnUndefined(undefined));
}
test "pointer to alias behaves same as pointer to function" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
const S = struct {
fn foo() u32 {
return 11227;
}
const bar = foo;
};
var a = &S.bar;
_ = &a;
try std.testing.expect(S.foo() == a());
}
test "comptime parameters don't have to be marked comptime if only called at comptime" {
const S = struct {
fn foo(x: comptime_int, y: comptime_int) u32 {
return x + y;
}
};
comptime std.debug.assert(S.foo(5, 6) == 11);
}
test "inline function with comptime-known comptime-only return type called at runtime" {
const S = struct {
inline fn foo(x: *i32, y: *const i32) type {
x.* = y.*;
return f32;
}
};
var a: i32 = 0;
const b: i32 = 111;
const T = S.foo(&a, &b);
try expectEqual(111, a);
try expectEqual(f32, T);
}
test "address of function parameter is consistent" {
const S = struct {
fn paramAddrMatch(x: u8) bool {
return &x == &x;
}
};
try expect(S.paramAddrMatch(0));
comptime assert(S.paramAddrMatch(0));
}
test "address of function parameter is consistent in other parameter type" {
const S = struct {
fn paramAddrMatch(comptime x: u8, y: if (&x != &x) unreachable else u8) void {
_ = y;
}
};
S.paramAddrMatch(1, 2);
}
test "address of function parameter is consistent in function return type" {
const S = struct {
fn paramAddrMatch(comptime x: u8) if (&x != &x) unreachable else void {}
};
S.paramAddrMatch(1);
}
test "function parameter self equality" {
const S = struct {
fn equal(x: u32) bool {
return x == x;
}
fn notEqual(x: u32) bool {
return x != x;
}
fn lessThan(x: u32) bool {
return x < x;
}
fn lessThanOrEqual(x: u32) bool {
return x <= x;
}
fn greaterThan(x: u32) bool {
return x > x;
}
fn greaterThanOrEqual(x: u32) bool {
return x >= x;
}
};
try expect(S.equal(42));
try expect(!S.notEqual(42));
try expect(!S.lessThan(42));
try expect(S.lessThanOrEqual(42));
try expect(!S.greaterThan(42));
try expect(S.greaterThanOrEqual(42));
}
test "inline call propagates comptime-known argument to generic parameter and return types" {
const S = struct {
inline fn f(x: bool, y: if (x) u8 else u16) if (x) bool else u32 {
if (x) {
comptime assert(@TypeOf(y) == u8);
return y == 0;
} else {
comptime assert(@TypeOf(y) == u16);
return y * 10;
}
}
fn g(x: bool, y: if (x) u8 else u16) if (x) bool else u32 {
if (x) {
comptime assert(@TypeOf(y) == u8);
return y == 0;
} else {
comptime assert(@TypeOf(y) == u16);
return y * 10;
}
}
};
const a0 = S.f(true, 200); // false
const a1 = S.f(false, 1234); // 12340
const b0 = @call(.always_inline, S.g, .{ true, 200 }); // false
const b1 = @call(.always_inline, S.g, .{ false, 1234 }); // 12340
comptime assert(@TypeOf(a0) == bool);
comptime assert(@TypeOf(b0) == bool);
try expect(a0 == false);
try expect(b0 == false);
comptime assert(@TypeOf(a1) == u32);
comptime assert(@TypeOf(b1) == u32);
try expect(a1 == 12340);
try expect(b1 == 12340);
}