zig/test/behavior/call.zig
mlugg f2c8fa769a
Sema: refactor generic calls to interleave argument analysis and parameter type resolution
AstGen provides all function call arguments with a result location,
referenced through the call instruction index. The idea is that this
should be the parameter type, but for `anytype` parameters, we use
generic poison, which is required to be handled correctly.

Previously, generic instantiations and inline calls worked by evaluating
all args in advance, before resolving generic parameter types. This
means any generic parameter (not just `anytype` ones) had generic poison
result types. This caused missing result locations in some cases.

Additionally, the generic instantiation logic caused `zirParam` to
analyze the argument types a second time before coercion. This meant
that for nominal types (struct/enum/etc), a *new* type was created,
distinct to the result type which was previously forwarded to the
argument expression.

This commit fixes both of these issues. Generic parameter type
resolution is now interleaved with argument analysis, so that we don't
have unnecessary generic poison types, and generic instantiation logic
now handles parameters itself rather than falling through to the
standard zirParam logic, so avoids duplicating the types.

Resolves: #16566
Resolves: #16258
Resolves: #16753
2023-08-10 10:00:26 +01:00

494 lines
15 KiB
Zig

const builtin = @import("builtin");
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
test "super basic invocations" {
const foo = struct {
fn foo() i32 {
return 1234;
}
}.foo;
try expect(@call(.auto, foo, .{}) == 1234);
try comptime expect(@call(.always_inline, foo, .{}) == 1234);
{
// comptime call without comptime keyword
const result = @call(.compile_time, foo, .{}) == 1234;
try comptime expect(result);
}
}
test "basic invocations" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
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 foo = struct {
fn foo() i32 {
return 1234;
}
}.foo;
try expect(@call(.auto, foo, .{}) == 1234);
comptime {
// modifiers that allow comptime calls
try expect(@call(.auto, foo, .{}) == 1234);
try expect(@call(.no_async, foo, .{}) == 1234);
try expect(@call(.always_tail, foo, .{}) == 1234);
try expect(@call(.always_inline, foo, .{}) == 1234);
}
{
// comptime call without comptime keyword
const result = @call(.compile_time, foo, .{}) == 1234;
try comptime expect(result);
}
{
// call of non comptime-known function
var alias_foo = &foo;
try expect(@call(.no_async, alias_foo, .{}) == 1234);
try expect(@call(.never_tail, alias_foo, .{}) == 1234);
try expect(@call(.never_inline, alias_foo, .{}) == 1234);
}
}
test "tuple parameters" {
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 add = struct {
fn add(a: i32, b: i32) i32 {
return a + b;
}
}.add;
var a: i32 = 12;
var b: i32 = 34;
try expect(@call(.auto, add, .{ a, 34 }) == 46);
try expect(@call(.auto, add, .{ 12, b }) == 46);
try expect(@call(.auto, add, .{ a, b }) == 46);
try expect(@call(.auto, add, .{ 12, 34 }) == 46);
if (false) {
try comptime expect(@call(.auto, add, .{ 12, 34 }) == 46); // TODO
}
try expect(comptime @call(.auto, add, .{ 12, 34 }) == 46);
{
const separate_args0 = .{ a, b };
const separate_args1 = .{ a, 34 };
const separate_args2 = .{ 12, 34 };
const separate_args3 = .{ 12, b };
try expect(@call(.always_inline, add, separate_args0) == 46);
try expect(@call(.always_inline, add, separate_args1) == 46);
try expect(@call(.always_inline, add, separate_args2) == 46);
try expect(@call(.always_inline, add, separate_args3) == 46);
}
}
test "result location of function call argument through runtime condition and struct init" {
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 E = enum { a, b };
const S = struct {
e: E,
};
const namespace = struct {
fn foo(s: S) !void {
try expect(s.e == .b);
}
};
var runtime = true;
try namespace.foo(.{
.e = if (!runtime) .a else .b,
});
}
test "function call with 40 arguments" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn doTheTest(thirty_nine: i32) !void {
const result = add(
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
thirty_nine,
40,
);
try expect(result == 820);
try expect(thirty_nine == 39);
}
fn add(
a0: i32,
a1: i32,
a2: i32,
a3: i32,
a4: i32,
a5: i32,
a6: i32,
a7: i32,
a8: i32,
a9: i32,
a10: i32,
a11: i32,
a12: i32,
a13: i32,
a14: i32,
a15: i32,
a16: i32,
a17: i32,
a18: i32,
a19: i32,
a20: i32,
a21: i32,
a22: i32,
a23: i32,
a24: i32,
a25: i32,
a26: i32,
a27: i32,
a28: i32,
a29: i32,
a30: i32,
a31: i32,
a32: i32,
a33: i32,
a34: i32,
a35: i32,
a36: i32,
a37: i32,
a38: i32,
a39: i32,
a40: i32,
) i32 {
return a0 +
a1 +
a2 +
a3 +
a4 +
a5 +
a6 +
a7 +
a8 +
a9 +
a10 +
a11 +
a12 +
a13 +
a14 +
a15 +
a16 +
a17 +
a18 +
a19 +
a20 +
a21 +
a22 +
a23 +
a24 +
a25 +
a26 +
a27 +
a28 +
a29 +
a30 +
a31 +
a32 +
a33 +
a34 +
a35 +
a36 +
a37 +
a38 +
a39 +
a40;
}
};
try S.doTheTest(39);
}
test "arguments to comptime parameters generated in comptime blocks" {
const S = struct {
fn fortyTwo() i32 {
return 42;
}
fn foo(comptime x: i32) void {
if (x != 42) @compileError("bad");
}
};
S.foo(S.fortyTwo());
}
test "forced tail call" {
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_c) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_llvm) {
// Only attempt this test on targets we know have tail call support in LLVM.
if (builtin.cpu.arch != .x86_64 and builtin.cpu.arch != .aarch64) {
return error.SkipZigTest;
}
}
const S = struct {
fn fibonacciTailInternal(n: u16, a: u16, b: u16) u16 {
if (n == 0) return a;
if (n == 1) return b;
return @call(
.always_tail,
fibonacciTailInternal,
.{ n - 1, b, a + b },
);
}
fn fibonacciTail(n: u16) u16 {
return fibonacciTailInternal(n, 0, 1);
}
};
try expect(S.fibonacciTail(10) == 55);
}
test "inline call preserves tail call" {
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_c) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_llvm) {
// Only attempt this test on targets we know have tail call support in LLVM.
if (builtin.cpu.arch != .x86_64 and builtin.cpu.arch != .aarch64) {
return error.SkipZigTest;
}
}
const max = std.math.maxInt(u16);
const S = struct {
var a: u16 = 0;
fn foo() void {
return bar();
}
inline fn bar() void {
if (a == max) return;
// Stack overflow if not tail called
var buf: [max]u16 = undefined;
buf[a] = a;
a += 1;
return @call(.always_tail, foo, .{});
}
};
S.foo();
try expect(S.a == std.math.maxInt(u16));
}
test "inline call doesn't re-evaluate non generic struct" {
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_spirv64) return error.SkipZigTest;
const S = struct {
fn foo(f: struct { a: u8, b: u8 }) !void {
try expect(f.a == 123);
try expect(f.b == 45);
}
};
const ArgTuple = std.meta.ArgsTuple(@TypeOf(S.foo));
try @call(.always_inline, S.foo, ArgTuple{.{ .a = 123, .b = 45 }});
try comptime @call(.always_inline, S.foo, ArgTuple{.{ .a = 123, .b = 45 }});
}
test "Enum constructed by @Type passed as generic argument" {
const S = struct {
const E = std.meta.FieldEnum(struct {
prev_pos: bool,
pos: bool,
vel: bool,
damp_vel: bool,
acc: bool,
rgba: bool,
prev_scale: bool,
scale: bool,
prev_rotation: bool,
rotation: bool,
angular_vel: bool,
alive: bool,
});
fn foo(comptime a: E, b: u32) !void {
try expect(@intFromEnum(a) == b);
}
};
inline for (@typeInfo(S.E).Enum.fields, 0..) |_, i| {
try S.foo(@as(S.E, @enumFromInt(i)), i);
}
}
test "generic function with generic function parameter" {
const S = struct {
fn f(comptime a: fn (anytype) anyerror!void, b: anytype) anyerror!void {
try a(b);
}
fn g(a: anytype) anyerror!void {
try expect(a == 123);
}
};
try S.f(S.g, 123);
}
test "recursive inline call with comptime known argument" {
const S = struct {
inline fn foo(x: i32) i32 {
if (x <= 0) {
return 0;
} else {
return x * 2 + foo(x - 1);
}
}
};
try expect(S.foo(4) == 20);
}
test "inline while with @call" {
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_spirv64) return error.SkipZigTest;
const S = struct {
fn inc(a: *u32) void {
a.* += 1;
}
};
var a: u32 = 0;
comptime var i = 0;
inline while (i < 10) : (i += 1) {
@call(.auto, S.inc, .{&a});
}
try expect(a == 10);
}
test "method call as parameter type" {
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
const S = struct {
fn foo(x: anytype, y: @TypeOf(x).Inner()) @TypeOf(y) {
return y;
}
fn Inner() type {
return u64;
}
};
try expectEqual(@as(u64, 123), S.foo(S{}, 123));
try expectEqual(@as(u64, 500), S.foo(S{}, 500));
}
test "non-anytype generic parameters provide result type" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
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; // TODO
const S = struct {
fn f(comptime T: type, y: T) !void {
try expectEqual(@as(T, 123), y);
}
fn g(x: anytype, y: @TypeOf(x)) !void {
try expectEqual(@as(@TypeOf(x), 0x222), y);
}
};
var rt_u16: u16 = 123;
var rt_u32: u32 = 0x10000222;
try S.f(u8, @intCast(rt_u16));
try S.f(u8, @intCast(123));
try S.g(rt_u16, @truncate(rt_u32));
try S.g(rt_u16, @truncate(0x10000222));
try comptime S.f(u8, @intCast(123));
try comptime S.g(@as(u16, undefined), @truncate(0x99990222));
}
test "argument to generic function has correct result type" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
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; // TODO
const S = struct {
fn foo(_: anytype, e: enum { a, b }) bool {
return e == .b;
}
fn doTheTest() !void {
var t = true;
// Since the enum literal passes through a runtime conditional here, these can only
// compile if RLS provides the correct result type to the argument
try expect(foo({}, if (!t) .a else .b));
try expect(!foo("dummy", if (t) .a else .b));
try expect(foo({}, if (t) .b else .a));
try expect(!foo(123, if (t) .a else .a));
try expect(foo(123, if (t) .b else .b));
}
};
try S.doTheTest();
try comptime S.doTheTest();
}