authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-01-12 20:55:30+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-01-13 02:57:15+00:00
logf78f9388fe79f084d5ea028e6270a410eacfc316
tree40b3eeb986f9fdc8866a9d41134b41801a5adc84
parent15fe99957226aa27da838812ec891f11b299453d

Sema: allow tail calls of function pointers

Resolves: #22474

4 files changed, 110 insertions(+), 74 deletions(-)

src/Sema.zig+7-1
...@@ -7983,7 +7983,13 @@ fn analyzeCall(...@@ -7983,7 +7983,13 @@ fn analyzeCall(
7983 }7983 }
79847984
7985 if (call_tag == .call_always_tail) {7985 if (call_tag == .call_always_tail) {
7986 return sema.handleTailCall(block, call_src, sema.typeOf(runtime_func), result);7986 const func_or_ptr_ty = sema.typeOf(runtime_func);
7987 const runtime_func_ty = switch (func_or_ptr_ty.zigTypeTag(zcu)) {
7988 .@"fn" => func_or_ptr_ty,
7989 .pointer => func_or_ptr_ty.childType(zcu),
7990 else => unreachable,
7991 };
7992 return sema.handleTailCall(block, call_src, runtime_func_ty, result);
7987 }7993 }
79887994
7989 if (resolved_ret_ty.toIntern() == .noreturn_type) {7995 if (resolved_ret_ty.toIntern() == .noreturn_type) {
test/behavior.zig-1
...@@ -17,7 +17,6 @@ test {...@@ -17,7 +17,6 @@ test {
17 _ = @import("behavior/byteswap.zig");17 _ = @import("behavior/byteswap.zig");
18 _ = @import("behavior/byval_arg_var.zig");18 _ = @import("behavior/byval_arg_var.zig");
19 _ = @import("behavior/call.zig");19 _ = @import("behavior/call.zig");
20 _ = @import("behavior/call_tail.zig");
21 _ = @import("behavior/cast.zig");20 _ = @import("behavior/cast.zig");
22 _ = @import("behavior/cast_int.zig");21 _ = @import("behavior/cast_int.zig");
23 _ = @import("behavior/comptime_memory.zig");22 _ = @import("behavior/comptime_memory.zig");
test/behavior/call.zig+103
...@@ -651,3 +651,106 @@ test "function call with cast to anyopaque pointer" {...@@ -651,3 +651,106 @@ test "function call with cast to anyopaque pointer" {
651 };651 };
652 Foo.bar(Foo.t);652 Foo.bar(Foo.t);
653}653}
654
655test "arguments pointed to on stack into tailcall" {
656 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
657 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
658 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
659 if (builtin.zig_backend == .stage2_c and builtin.os.tag == .windows) return error.SkipZigTest; // MSVC doesn't support always tail calls
660
661 switch (builtin.cpu.arch) {
662 .wasm32,
663 .mips,
664 .mipsel,
665 .mips64,
666 .mips64el,
667 .powerpc,
668 .powerpcle,
669 .powerpc64,
670 .powerpc64le,
671 => return error.SkipZigTest,
672 else => {},
673 }
674
675 const S = struct {
676 var base: usize = undefined;
677 var result_off: [7]usize = undefined;
678 var result_len: [7]usize = undefined;
679 var result_index: usize = 0;
680
681 noinline fn insertionSort(data: []u64) void {
682 result_off[result_index] = @intFromPtr(data.ptr) - base;
683 result_len[result_index] = data.len;
684 result_index += 1;
685 if (data.len > 1) {
686 var least_i: usize = 0;
687 var i: usize = 1;
688 while (i < data.len) : (i += 1) {
689 if (data[i] < data[least_i])
690 least_i = i;
691 }
692 std.mem.swap(u64, &data[0], &data[least_i]);
693
694 // there used to be a bug where
695 // `data[1..]` is created on the stack
696 // and pointed to by the first argument register
697 // then stack is invalidated by the tailcall and
698 // overwritten by callee
699 // https://github.com/ziglang/zig/issues/9703
700 return @call(.always_tail, insertionSort, .{data[1..]});
701 }
702 }
703 };
704
705 var data = [_]u64{ 1, 6, 2, 7, 1, 9, 3 };
706 S.base = @intFromPtr(&data);
707 S.insertionSort(data[0..]);
708 try expect(S.result_len[0] == 7);
709 try expect(S.result_len[1] == 6);
710 try expect(S.result_len[2] == 5);
711 try expect(S.result_len[3] == 4);
712 try expect(S.result_len[4] == 3);
713 try expect(S.result_len[5] == 2);
714 try expect(S.result_len[6] == 1);
715
716 try expect(S.result_off[0] == 0);
717 try expect(S.result_off[1] == 8);
718 try expect(S.result_off[2] == 16);
719 try expect(S.result_off[3] == 24);
720 try expect(S.result_off[4] == 32);
721 try expect(S.result_off[5] == 40);
722 try expect(S.result_off[6] == 48);
723}
724
725test "tail call function pointer" {
726 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
727 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
728 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
729 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
730 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
731 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
732 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
733
734 if (builtin.zig_backend == .stage2_llvm) {
735 if (builtin.cpu.arch.isMIPS() or builtin.cpu.arch.isPowerPC() or builtin.cpu.arch.isWasm()) {
736 return error.SkipZigTest;
737 }
738 }
739
740 if (builtin.zig_backend == .stage2_c and builtin.os.tag == .windows) return error.SkipZigTest; // MSVC doesn't support always tail calls
741
742 const S = struct {
743 fn foo(n: u8) void {
744 if (n == 0) return;
745 const other: *const fn (u8) void = &bar;
746 return @call(.always_tail, other, .{n - 1});
747 }
748 fn bar(n: u8) void {
749 var other: *const fn (u8) void = undefined;
750 other = &foo; // runtime-known pointer
751 return @call(.always_tail, other, .{n});
752 }
753 };
754
755 S.foo(100);
756}
test/behavior/call_tail.zig deleted-72
...@@ -1,72 +0,0 @@
1const builtin = @import("builtin");
2const std = @import("std");
3const expect = std.testing.expect;
4
5var base: usize = undefined;
6var result_off: [7]usize = undefined;
7var result_len: [7]usize = undefined;
8var result_index: usize = 0;
9
10noinline fn insertionSort(data: []u64) void {
11 result_off[result_index] = @intFromPtr(data.ptr) - base;
12 result_len[result_index] = data.len;
13 result_index += 1;
14 if (data.len > 1) {
15 var least_i: usize = 0;
16 var i: usize = 1;
17 while (i < data.len) : (i += 1) {
18 if (data[i] < data[least_i])
19 least_i = i;
20 }
21 std.mem.swap(u64, &data[0], &data[least_i]);
22
23 // there used to be a bug where
24 // `data[1..]` is created on the stack
25 // and pointed to by the first argument register
26 // then stack is invalidated by the tailcall and
27 // overwritten by callee
28 // https://github.com/ziglang/zig/issues/9703
29 return @call(.always_tail, insertionSort, .{data[1..]});
30 }
31}
32
33test "arguments pointed to on stack into tailcall" {
34 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
35
36 switch (builtin.cpu.arch) {
37 .wasm32,
38 .mips,
39 .mipsel,
40 .mips64,
41 .mips64el,
42 .powerpc,
43 .powerpcle,
44 .powerpc64,
45 .powerpc64le,
46 => return error.SkipZigTest,
47 else => {},
48 }
49 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest;
50 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
51
52 if (builtin.zig_backend == .stage2_c and builtin.os.tag == .windows) return error.SkipZigTest; // MSVC doesn't support always tail calls
53
54 var data = [_]u64{ 1, 6, 2, 7, 1, 9, 3 };
55 base = @intFromPtr(&data);
56 insertionSort(data[0..]);
57 try expect(result_len[0] == 7);
58 try expect(result_len[1] == 6);
59 try expect(result_len[2] == 5);
60 try expect(result_len[3] == 4);
61 try expect(result_len[4] == 3);
62 try expect(result_len[5] == 2);
63 try expect(result_len[6] == 1);
64
65 try expect(result_off[0] == 0);
66 try expect(result_off[1] == 8);
67 try expect(result_off[2] == 16);
68 try expect(result_off[3] == 24);
69 try expect(result_off[4] == 32);
70 try expect(result_off[5] == 40);
71 try expect(result_off[6] == 48);
72}